From 0d92719bd8fd951031f26c7819a9eab233cbca73 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 21 Aug 2015 16:02:25 -0400 Subject: [PATCH 01/53] Version Bump --- sharpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharpy/__init__.py b/sharpy/__init__.py index 1bc90e9..c07c373 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 8) +VERSION = (0, 9) From 92d66203e33ed20e5c675ef1574f29f39802025e Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 21 Aug 2015 16:03:46 -0400 Subject: [PATCH 02/53] Add promotion parsers --- sharpy/parsers.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/sharpy/parsers.py b/sharpy/parsers.py index 6d4b417..8235e93 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -328,4 +328,79 @@ def parse_subscription_item(self, item_element): item['modified_datetime'] = self.parse_datetime(item_element.findtext('modifiedDatetime')) return item + + +class PromotionsParser(CheddarOutputParser): + ''' + A utility class for parsing cheddar's xml output for promotions. + ''' + def parse_xml(self, xml_str): + promotions = [] + promotions_xml = XML(xml_str) + for promotion_xml in promotions_xml: + promotion = self.parse_promotion(promotion_xml) + promotions.append(promotion) + + return promotions + + def parse_promotions(self, promotions_element): + promotions = [] + if promotions_element is not None: + for promotion_element in promotions_element: + promotions.append(self.parse_promotion(promotion_element)) + + return promotions + + def parse_promotion(self, promotion_element): + promotion = {} + import pdb; pdb.set_trace() + promotion['id'] = promotion_element.attrib['id'] + promotion['name'] = promotion_element.findtext('name') + promotion['description'] = promotion_element.findtext('description') + promotion['created_datetime'] = self.parse_datetime(promotion_element.findtext('createdDatetime')) + + promotion['incentives'] = self.parse_incentives(promotion_element.find('incentives')) + promotion['coupons'] = self.parse_coupons(promotion_element.find('coupons')) + + return promotion + + def parse_incentives(self, incentives_element): + incentives = [] + + if incentives_element is not None: + for incentive_element in incentives_element: + incentives.append(self.parse_incentive(incentive_element)) + + return incentives + + def parse_incentive(self, incentive_element): + incentive = {} + + incentive['id'] = incentive_element.attrib['id'] + incentive['type'] = incentive_element.findtext('type') + incentive['percentage'] = incentive_element.findtext('percentage') + incentive['months'] = incentive_element.findtext('months') + incentive['created_datetime'] = self.parse_datetime(incentive_element.findtext('createdDatetime')) + + return incentive + + def parse_coupons(self, coupons_element): + coupons = [] + + if coupons_element is not None: + for coupon_element in coupons_element: + coupons.append(self.parse_coupon(coupon_element)) + + return coupons + + def parse_coupon(self, coupon_element): + coupon = {} + + coupon['id'] = coupon_element.attrib['id'] + coupon['code'] = coupon_element.attrib['code'] + coupon['max_redemptions'] = coupon_element.findtext('maxRedemptions') + coupon['expiration_datetime'] = self.parse_datetime(coupon_element.findtext('expirationDatetime')) + coupon['created_datetime'] = self.parse_datetime(coupon_element.findtext('createdDatetime')) + + return coupon From 7f627b2cd905cb44938e9aaf7327daf5f60b3f18 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 21 Aug 2015 16:04:01 -0400 Subject: [PATCH 03/53] Add promotion object to product. --- sharpy/product.py | 77 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/sharpy/product.py b/sharpy/product.py index cd6c0bc..17992b9 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -7,7 +7,7 @@ from sharpy.client import Client from sharpy.exceptions import NotFound -from sharpy.parsers import PlansParser, CustomersParser +from sharpy.parsers import PlansParser, CustomersParser, PromotionsParser class CheddarProduct(object): @@ -263,8 +263,42 @@ def delete_all_customers(self): path='customers/delete-all/confirm/%d' % int(time()), method='POST' ) - - + + def get_all_promotions(self): + ''' + Returns all promotions. + https://cheddargetter.com/developers#promotions + ''' + promotions = [] + + try: + response = self.client.make_request(path='promotions/get') + except NotFound: + response = None + + response = self.client.make_request(path='promotions/get') + promotions_parser = PromotionsParser() + promotions_data = promotions_parser.parse_xml(response.content) + promotions = [Promotion(**promotion_data) for promotion_data in promotions_data] + + return promotions + + def get_promotion(self, code): + ''' + Get the promotion with the specified coupon code. + https://cheddargetter.com/developers#single-promotion + ''' + + response = self.client.make_request( + path='promotions/get', + params={'code': code}, + ) + promotion_parser = PromotionsParser() + promotion_data = promotion_parser.parse_xml(response.content) + + return Promotion(**promotion_data[0]) + + class PricingPlan(object): def __init__(self, name, code, id, description, is_active, is_free, @@ -576,12 +610,12 @@ def __init__(self, id, gateway_token, cc_first_name, cc_last_name, super(Subscription, self).__init__() - def load_data(self, id, gateway_token, cc_first_name, cc_last_name, \ - cc_company, cc_country, cc_address, cc_city, cc_state, \ - cc_zip, cc_type, cc_last_four, cc_expiration_date, customer,\ - cc_email=None, canceled_datetime=None ,created_datetime=None, \ - plans=None, invoices=None, items=None, gateway_account=None, \ - cancel_reason=None, cancel_type=None, redirect_url=None): + def load_data(self, id, gateway_token, cc_first_name, cc_last_name, + cc_company, cc_country, cc_address, cc_city, cc_state, + cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, + cc_email=None, canceled_datetime=None ,created_datetime=None, + plans=None, invoices=None, items=None, gateway_account=None, + cancel_reason=None, cancel_type=None, redirect_url=None): self.id = id self.gateway_token = gateway_token @@ -764,5 +798,26 @@ def set(self, quantity): ) return self.subscription.customer.load_data_from_xml(response.content) - - + + +class Promotion(object): + def __init__(self, id=None, code=None, name=None, description=None, + created_datetime=None, incentives=None, coupons=None): + + self.load_data(code=code, id=id, name=name, description=description, + created_datetime=created_datetime, + incentives=incentives, coupons=coupons) + + super(Promotion, self).__init__() + + def load_data(self, id=None, code=None, name=None, description=None, + created_datetime=None, incentives=None, coupons=None): + + self.code = code + self.id = id + self.name = name + self.description = description + self.created = created_datetime + + self.incentives = incentives + self.coupons = coupons \ No newline at end of file From 8b589acf3348a94af9d56bc4e837b518fa8d3243 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 21 Aug 2015 16:10:41 -0400 Subject: [PATCH 04/53] Update readme with elementtree information --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 630b8bb..25f40b2 100644 --- a/README.rst +++ b/README.rst @@ -50,8 +50,17 @@ Code You can checkout and download Sharpy's latest code at `Github `_. +Installing elementtree for Development and Unit Testing +======================================================= +When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. + + wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip + unzip elementtree-1.2.6-20050316.zip + cd elementtree-1.2.6-20050316/ + pip install . + TODOs ===== * Flesh out the documentation to cover the full API. -* Add support for the various filtering options in the `get_customers` call. \ No newline at end of file +* Add support for the various filtering options in the `get_customers` call. From 72f6aa8d6bf050cd0b661c382de4114c9593691e Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 21 Aug 2015 16:13:06 -0400 Subject: [PATCH 05/53] Formatting updates for code in readme --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 25f40b2..c5dcb99 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,8 @@ Getting Started To get started with Sharpy, simply install it like you would any other python package +.. code:: + pip install sharpy Optionally, you can also install `lxml `_ on your @@ -54,6 +56,8 @@ Installing elementtree for Development and Unit Testing ======================================================= When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. +.. code:: + wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip unzip elementtree-1.2.6-20050316.zip cd elementtree-1.2.6-20050316/ From 02b729539b2375998d411adfc9753401d455de71 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 21 Aug 2015 17:28:38 -0400 Subject: [PATCH 06/53] Add readme for testing --- tests/readme.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/readme.rst diff --git a/tests/readme.rst b/tests/readme.rst new file mode 100644 index 0000000..6b41c7e --- /dev/null +++ b/tests/readme.rst @@ -0,0 +1,31 @@ +Requirements +============ + +Inside a virtualenv, run: + +pip install -r dev-requirements.txt + +Installing elementtree for Unit Testing +======================================================= +When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. + +.. code:: + + wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip + unzip elementtree-1.2.6-20050316.zip + cd elementtree-1.2.6-20050316/ + pip install . + +Config +====== + +In the tests folder, copy the config.ini.template to config.ini. Fill in your email, password, and product code. + +You will also need to setup the correct plans in cheddar. You may want to set up a product intended just for testing. + +Running Tests +============= +Run the test with nosetests. + +.. code:: + nosetests From ded5aed84cd42c9728bc7ac0c281ab34714208b0 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 21 Aug 2015 17:29:37 -0400 Subject: [PATCH 07/53] Needs line break for code to show --- tests/readme.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/readme.rst b/tests/readme.rst index 6b41c7e..48e1dfc 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -28,4 +28,5 @@ Running Tests Run the test with nosetests. .. code:: + nosetests From acfc60cda7601530fed205e7fcb11b496b7de357 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 21 Aug 2015 17:30:10 -0400 Subject: [PATCH 08/53] Add code formatting to install line. --- tests/readme.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/readme.rst b/tests/readme.rst index 48e1dfc..241072c 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -3,7 +3,9 @@ Requirements Inside a virtualenv, run: -pip install -r dev-requirements.txt +.. code:: + + pip install -r dev-requirements.txt Installing elementtree for Unit Testing ======================================================= From 7ffa8a2be17463a4b810c1bf3c799082d02d73a5 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 11:11:47 -0400 Subject: [PATCH 09/53] Update readme with cheddargetter unittest setup. --- tests/readme.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/readme.rst b/tests/readme.rst index 241072c..6947dc4 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -23,8 +23,18 @@ Config In the tests folder, copy the config.ini.template to config.ini. Fill in your email, password, and product code. + +Cheddar Setup +============= You will also need to setup the correct plans in cheddar. You may want to set up a product intended just for testing. +The following plan codes are required for unit tests: + +* FREE_MONTHLY +* PAID_MONTHLY + +Be sure you turn on the native gateway credit card option. + Running Tests ============= Run the test with nosetests. From 018c6daa6e19eb902c0e5affbe06bf104c03e6a1 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 11:12:39 -0400 Subject: [PATCH 10/53] CheddarGetter setup will probably happen before config. --- tests/readme.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/readme.rst b/tests/readme.rst index 6947dc4..4accffd 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -18,13 +18,7 @@ When trying to install elementtree, pip may report that there is no such package cd elementtree-1.2.6-20050316/ pip install . -Config -====== - -In the tests folder, copy the config.ini.template to config.ini. Fill in your email, password, and product code. - - -Cheddar Setup +CheddarGetter Setup ============= You will also need to setup the correct plans in cheddar. You may want to set up a product intended just for testing. @@ -35,6 +29,11 @@ The following plan codes are required for unit tests: Be sure you turn on the native gateway credit card option. +Config +====== + +In the tests folder, copy the config.ini.template to config.ini. Fill in your email, password, and product code. + Running Tests ============= Run the test with nosetests. From 35fa3cc21be40a712b491a15ea9a291f07d5bb35 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 13:15:12 -0400 Subject: [PATCH 11/53] Update readme.rst --- tests/readme.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/readme.rst b/tests/readme.rst index 4accffd..a126579 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -26,6 +26,7 @@ The following plan codes are required for unit tests: * FREE_MONTHLY * PAID_MONTHLY +* TRACKED_MONTHLY Be sure you turn on the native gateway credit card option. From 6926b4f9dd7ce032661e7582854e5ef0829fd8bd Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 13:18:27 -0400 Subject: [PATCH 12/53] Update readme.rst --- tests/readme.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/readme.rst b/tests/readme.rst index a126579..08e2a8a 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -28,6 +28,11 @@ The following plan codes are required for unit tests: * PAID_MONTHLY * TRACKED_MONTHLY +The following tracked items are required for unit tests: + +* MONTHLY_ITEM +* ONCE_ITEM + Be sure you turn on the native gateway credit card option. Config From 4d0fd823206337b98bf1d32bfeb313fcaff532e7 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 13:43:45 -0400 Subject: [PATCH 13/53] Try to be more clean on plans. Update the plans to a table to include the names and number of tracked items. Some Unit tests are checking against these. --- tests/readme.rst | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/readme.rst b/tests/readme.rst index 08e2a8a..d865c99 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -22,18 +22,31 @@ CheddarGetter Setup ============= You will also need to setup the correct plans in cheddar. You may want to set up a product intended just for testing. -The following plan codes are required for unit tests: -* FREE_MONTHLY -* PAID_MONTHLY -* TRACKED_MONTHLY The following tracked items are required for unit tests: -* MONTHLY_ITEM -* ONCE_ITEM ++--------------+--------------+ +| Name | Code | ++==============+==============+ +| Once Item | ONCE_ITEM | ++--------------+--------------+ +| Monthly Item | MONTHLY_ITEM | ++--------------+--------------+ + +The following plan codes are required for unit tests: -Be sure you turn on the native gateway credit card option. ++-----------------+-----------------+---------+-----------+--------------+ +| Plan Name | Code | Price | ONCE_ITEM | MONTHLY_ITEM | ++=================+=================+=========+===========+==============+ +| Free Monthly | FREE_MONTHLY | $0.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ +| Paid Monthly | PAID_MONTHLY | $10.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ +| Tracked Monthly | TRACKED_MONTHLY | $10.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ + +Be sure to turn on the native gateway credit card option in Configuration > Product settings > Gateway Settings. Config ====== From fbed197cc3e06d64c9bc24753389ffd608f58aaa Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 14:31:42 -0400 Subject: [PATCH 14/53] Update readme.rst --- tests/readme.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/readme.rst b/tests/readme.rst index d865c99..4d03d60 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -46,6 +46,17 @@ The following plan codes are required for unit tests: | Tracked Monthly | TRACKED_MONTHLY | $10.00 | 1 | 10 | +-----------------+-----------------+---------+-----------+--------------+ + +The following promotions are required for unit tests: + ++----------------+---------------+--------+ +| Promotion Name | Coupon Code | % Off | ++================+===============+========+ +| Coupon | COUPON | 10 | ++----------------+---------------+--------+ +| Coupon 2 | COUPON2 | 20 | ++----------------+---------------+--------+ + Be sure to turn on the native gateway credit card option in Configuration > Product settings > Gateway Settings. Config From ed573e111589bb398ae1b0b610fcdf06cdaf55ba Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 14:32:46 -0400 Subject: [PATCH 15/53] Update readme.rst --- tests/readme.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/readme.rst b/tests/readme.rst index 4d03d60..59bb58e 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -49,13 +49,13 @@ The following plan codes are required for unit tests: The following promotions are required for unit tests: -+----------------+---------------+--------+ -| Promotion Name | Coupon Code | % Off | -+================+===============+========+ -| Coupon | COUPON | 10 | -+----------------+---------------+--------+ -| Coupon 2 | COUPON2 | 20 | -+----------------+---------------+--------+ ++----------------+---------------+--------+-----------+ +| Promotion Name | Coupon Code | % Off | Duration | ++================+===============+========+===========+ +| Coupon | COUPON | 10 | Forever | ++----------------+---------------+--------+-----------+ +| Coupon 2 | COUPON2 | 20 | Forever | ++----------------+---------------+--------+-----------+ Be sure to turn on the native gateway credit card option in Configuration > Product settings > Gateway Settings. From f8f0500d116d6597660988d8bb9efd8857c079e1 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 15:17:34 -0400 Subject: [PATCH 16/53] Remove the debug code. --- sharpy/parsers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sharpy/parsers.py b/sharpy/parsers.py index 8235e93..7183c1f 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -354,7 +354,6 @@ def parse_promotions(self, promotions_element): def parse_promotion(self, promotion_element): promotion = {} - import pdb; pdb.set_trace() promotion['id'] = promotion_element.attrib['id'] promotion['name'] = promotion_element.findtext('name') promotion['description'] = promotion_element.findtext('description') From 12d08b322cd163a3b14a7d3e88fd0866b3705583 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 15:18:47 -0400 Subject: [PATCH 17/53] Don't request response twice. Verify response is return before trying to parse. Bubble up coupon code to promotion.code. --- sharpy/product.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/sharpy/product.py b/sharpy/product.py index 17992b9..f7810b9 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -276,10 +276,10 @@ def get_all_promotions(self): except NotFound: response = None - response = self.client.make_request(path='promotions/get') - promotions_parser = PromotionsParser() - promotions_data = promotions_parser.parse_xml(response.content) - promotions = [Promotion(**promotion_data) for promotion_data in promotions_data] + if response: + promotions_parser = PromotionsParser() + promotions_data = promotions_parser.parse_xml(response.content) + promotions = [Promotion(**promotion_data) for promotion_data in promotions_data] return promotions @@ -810,6 +810,9 @@ def __init__(self, id=None, code=None, name=None, description=None, super(Promotion, self).__init__() + def __unicode__(self): + return u'{0} ({1})'.format(self.name, self.code) + def load_data(self, id=None, code=None, name=None, description=None, created_datetime=None, incentives=None, coupons=None): @@ -820,4 +823,8 @@ def load_data(self, id=None, code=None, name=None, description=None, self.created = created_datetime self.incentives = incentives - self.coupons = coupons \ No newline at end of file + self.coupons = coupons + + # Bring coupon code up to parent promotion + if self.code is None and self.coupons and len(self.coupons) > 0: + self.code = self.coupons[0].get('code') From 6072a77cc4d19fa0855f8f23ee320894bd79b39f Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Mon, 24 Aug 2015 15:20:20 -0400 Subject: [PATCH 18/53] Add unit test for new methods. Skipping failing tests due to issues on cheddar's side. --- tests/client_tests.py | 13 +++- tests/product_tests.py | 161 ++++++++++++++++++++++++++++------------- 2 files changed, 120 insertions(+), 54 deletions(-) diff --git a/tests/client_tests.py b/tests/client_tests.py index 571cc64..10432ba 100644 --- a/tests/client_tests.py +++ b/tests/client_tests.py @@ -93,8 +93,9 @@ def test_make_request_access_denied(self): client = self.get_client(username=bad_username) client.make_request(path) - @raises(BadRequest) + @raises(NotFound) def test_make_request_bad_request(self): + """ Attempt to grab the plans without adding /get to the url. """ path = 'plans' client = self.get_client() client.make_request(path) @@ -104,7 +105,8 @@ def test_make_request_not_found(self): path = 'things-which-dont-exist' client = self.get_client() client.make_request(path) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_post_request(self): path = 'customers/new' @@ -118,7 +120,7 @@ def test_post_request(self): client = self.get_client() client.make_request(path, data=data) - + @unittest.skip('Skip until deleting customers is working') def generate_error_response(self, auxcode=None, path=None, params=None, **overrides): ''' Creates a request to cheddar which should return an error @@ -242,9 +244,12 @@ def test_format_datetime_with_now(self): self.assertEquals(expected, result) + @unittest.skip('I can not figure this out.') @clear_users def test_chedder_update_customer_error(self): - # Overriding the zipcode so a customer actually gets created + """ + Test overriding the zipcode so a customer actually gets updated. + """ overrides = { 'subscription[ccZip]': 12345 } diff --git a/tests/product_tests.py b/tests/product_tests.py index 16dcbc9..c0852e5 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -137,59 +137,73 @@ def get_customer_with_items(self, **kwargs): customer = self.get_customer(**data) return customer - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_simple_create_customer(self): self.get_customer() - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_company(self): self.get_customer(company='Test Co') - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_meta_data(self): self.get_customer(meta_data = {'key_1': 'value_1', 'key2': 'value_2'}) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_true_vat_exempt(self): self.get_customer(is_vat_exempt=True) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_false_vat_exempt(self): self.get_customer(is_vat_exempt=False) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_vat_number(self): self.get_customer(vat_number=12345) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_notes(self): self.get_customer(notes='This is a test note!') - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_first_contact_datetime(self): self.get_customer(first_contact_datetime=datetime.now()) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_referer(self): self.get_customer(referer='http://saaspire.com/test.html') - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_campaign_term(self): self.get_customer(campaign_term='testing') - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_campaign_name(self): self.get_customer(campaign_name='testing') - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_campaign_source(self): self.get_customer(campaign_source='testing') - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_campaign_content(self): self.get_customer(campaign_content='testing') - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_initial_bill_date(self): initial_bill_date = datetime.utcnow() + timedelta(days=60) @@ -201,11 +215,13 @@ def test_create_customer_with_initial_bill_date(self): # if the request is made around UTC midnight diff = initial_bill_date.date() - real_bill_date.date() self.assertLessEqual(diff.days, 1) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer(self): self.get_customer(**self.paid_defaults) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer_with_charges(self): data = copy(self.paid_defaults) @@ -214,7 +230,8 @@ def test_create_paid_customer_with_charges(self): charges.append({'code': 'charge2', 'quantity': 3, 'each_amount': 4}) data['charges'] = charges self.get_customer(**data) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer_with_decimal_charges(self): data = copy(self.paid_defaults) @@ -224,6 +241,7 @@ def test_create_paid_customer_with_decimal_charges(self): data['charges'] = charges self.get_customer(**data) + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer_with_items(self): data = copy(self.paid_defaults) @@ -234,7 +252,7 @@ def test_create_paid_customer_with_items(self): data['plan_code'] = 'TRACKED_MONTHLY' self.get_customer(**data) - + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer_with_decimal_quantity_items(self): data = copy(self.paid_defaults) @@ -245,11 +263,13 @@ def test_create_paid_customer_with_decimal_quantity_items(self): data['plan_code'] = 'TRACKED_MONTHLY' self.get_customer(**data) + @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paypal_customer(self): data = copy(self.paypal_defaults) self.get_customer(**data) + @unittest.skip('Skip until deleting customers is working') @clear_users def test_update_paypal_customer(self): data = copy(self.paypal_defaults) @@ -260,7 +280,7 @@ def test_update_paypal_customer(self): cancel_url='http://example.com/update-cancel/', ) - + @unittest.skip('Skip until deleting customers is working') @clear_users def test_customer_repr(self): customer = self.get_customer() @@ -269,7 +289,8 @@ def test_customer_repr(self): result = repr(customer) self.assertEquals(expected, result) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_subscription_repr(self): customer = self.get_customer() @@ -279,7 +300,8 @@ def test_subscription_repr(self): result = repr(subscription) self.assertIn(expected, result) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_pricing_plan_repr(self): customer = self.get_customer() @@ -290,8 +312,8 @@ def test_pricing_plan_repr(self): result = repr(plan) self.assertEquals(expected, result) - - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_item_repr(self): customer = self.get_customer_with_items() @@ -302,7 +324,8 @@ def test_item_repr(self): result = repr(item) self.assertEquals(expected, result) - + + @unittest.skip('Duplicate transaction error. 6 seconds.') @clear_users def test_get_customers(self): customer1 = self.get_customer() @@ -319,7 +342,8 @@ def test_get_customers(self): fetched_customers = product.get_customers() self.assertEquals(2, len(fetched_customers)) - + + @unittest.skip('Duplicate transaction error. 6 seconds.') @clear_users def test_get_customer(self): created_customer = self.get_customer() @@ -331,7 +355,8 @@ def test_get_customer(self): self.assertEquals(created_customer.first_name, fetched_customer.first_name) self.assertEquals(created_customer.last_name, fetched_customer.last_name) self.assertEquals(created_customer.email, fetched_customer.email) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_simple_customer_update(self): new_name = 'Different' @@ -343,7 +368,8 @@ def test_simple_customer_update(self): fetched_customer = product.get_customer(code=customer.code) self.assertEquals(customer.first_name, fetched_customer.first_name) - + + @unittest.skip('Skip until deleting customers is working') @clear_users @raises(NotFound) def test_delete_customer(self): @@ -355,8 +381,8 @@ def test_delete_customer(self): customer.delete() fetched_customer = product.get_customer(code=customer.code) - - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_delete_all_customers(self): customer_1 = self.get_customer() @@ -370,7 +396,8 @@ def test_delete_all_customers(self): fetched_customers = product.get_customers() self.assertEquals(0, len(fetched_customers)) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_cancel_subscription(self): customer = self.get_customer() @@ -398,19 +425,23 @@ def assert_increment(self, quantity=None): fetched_customer = product.get_customer(code=customer.code) fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_simple_increment(self): self.assert_increment() - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_int_increment(self): self.assert_increment(1) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_float_increment(self): self.assert_increment(1.234) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_decimal_increment(self): self.assert_increment(Decimal('1.234')) @@ -430,19 +461,23 @@ def assert_decrement(self, quantity=None): fetched_customer = product.get_customer(code=customer.code) fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_simple_decrement(self): self.assert_decrement() - + + @unittest.skip('Duplicate transaction error. 6 seconds.') @clear_users def test_int_decrement(self): self.assert_decrement(1) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_float_decrement(self): self.assert_decrement(1.234) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_decimal_decrement(self): self.assert_decrement(Decimal('1.234')) @@ -461,15 +496,18 @@ def assert_set(self, quantity): fetched_customer = product.get_customer(code=customer.code) fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + + @unittest.skip('Duplicate transaction error. 6 seconds.') @clear_users def test_int_set(self): self.assert_set(1) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_float_set(self): self.assert_set(1.234) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_decimal_set(self): self.assert_set(Decimal('1.234')) @@ -509,27 +547,28 @@ def assert_charged(self, code, each_amount, quantity=None, self.assertAlmostEqual(Decimal(each_amount), fetched_charge['each_amount'], places=2) self.assertEqual(quantity, fetched_charge['quantity']) self.assertEqual(description, fetched_charge['description']) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1) - - @clear_users - def test_add_float_charge(self): - self.assert_charged(code='TEST-CHARGE', each_amount=2.3, quantity=2) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_float_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=2.3, quantity=2) + @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_decimal_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=Decimal('2.3'), quantity=3) - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_charge_with_descriptions(self): self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1, description="A test charge") - + + @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_credit(self): self.assert_charged(code='TEST-CHARGE', each_amount=-1, quantity=1) @@ -574,6 +613,7 @@ def assertOneTimeInvoice(self, charges): invoice_type = 'one-time', ) + @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_simple_one_time_invoice(self): charges = [{ @@ -584,6 +624,7 @@ def test_add_simple_one_time_invoice(self): self.assertOneTimeInvoice(charges) + @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_one_time_invoice_with_description(self): charges = [{ @@ -595,7 +636,7 @@ def test_add_one_time_invoice_with_description(self): self.assertOneTimeInvoice(charges) - + @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_one_time_invoice_with_multiple_charges(self): charges = [{ @@ -612,3 +653,23 @@ def test_add_one_time_invoice_with_multiple_charges(self): },] self.assertOneTimeInvoice(charges) + + def test_get_all_promotions(self): + ''' Test get all promotions. ''' + product = self.get_product() + promotions = product.get_all_promotions() + + self.assertEquals(2, len(promotions)) + for promotion in promotions: + assert promotion.coupons[0].get('code') in ('COUPON', 'COUPON2') + + def test_get_promotion(self): + ''' Test get a single promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + self.assertEqual(unicode(promotion), 'Coupon (COUPON)') + self.assertEqual(promotion.name, 'Coupon') + self.assertEqual(promotion.coupons[0].get('code'), 'COUPON') + self.assertEqual(promotion.incentives[0].get('percentage'), '10') + self.assertEqual(promotion.incentives[0].get('expiration_datetime'), None) From 3a25cdf50c2c465fd1fd037426ffb3166c8836fd Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 13:33:26 -0400 Subject: [PATCH 19/53] Update readme.rst --- tests/readme.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/readme.rst b/tests/readme.rst index 59bb58e..325ac3d 100644 --- a/tests/readme.rst +++ b/tests/readme.rst @@ -58,6 +58,7 @@ The following promotions are required for unit tests: +----------------+---------------+--------+-----------+ Be sure to turn on the native gateway credit card option in Configuration > Product settings > Gateway Settings. +Be sure to turn on the paypal option in Configuration > Product settings > Gateway Settings or Quick Setup > Billing solution. I checked the "Use standard payments (PayPal account to PayPal account)" checkbox. Config ====== From 1824bcfa7585a1ebc72482ef0f0e66d9147aad54 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 15:11:56 -0400 Subject: [PATCH 20/53] parse_promotions is not needed because it is not used. --- sharpy/parsers.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sharpy/parsers.py b/sharpy/parsers.py index 7183c1f..52c7aa4 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -340,16 +340,7 @@ def parse_xml(self, xml_str): for promotion_xml in promotions_xml: promotion = self.parse_promotion(promotion_xml) promotions.append(promotion) - - return promotions - def parse_promotions(self, promotions_element): - promotions = [] - - if promotions_element is not None: - for promotion_element in promotions_element: - promotions.append(self.parse_promotion(promotion_element)) - return promotions def parse_promotion(self, promotion_element): From 84088308414b5da7024ff4a883860d1832e5a206 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 15:12:45 -0400 Subject: [PATCH 21/53] The convention used in the other classes is to include a __repr__. Include a __repr__ for Promotion. --- sharpy/product.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sharpy/product.py b/sharpy/product.py index f7810b9..817260d 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -810,6 +810,9 @@ def __init__(self, id=None, code=None, name=None, description=None, super(Promotion, self).__init__() + def __repr__(self): + return u'Promotion: %s (%s)' % (self.name, self.code,) + def __unicode__(self): return u'{0} ({1})'.format(self.name, self.code) From 721cd389db642be17e4b8f4c4d6b4945cbadb7c2 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 15:14:56 -0400 Subject: [PATCH 22/53] Also consider clearing customers a failure if success is not in the results. Related to a specific cheddar bug where it only returns . --- tests/testing_tools/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testing_tools/utils.py b/tests/testing_tools/utils.py index faa96bf..0b5d616 100644 --- a/tests/testing_tools/utils.py +++ b/tests/testing_tools/utils.py @@ -16,5 +16,5 @@ def clear_users(): response, content = h.request(url, 'POST') - if response.status != 200: - raise Exception('Could not clear users. Recieved a response of %s %s ' % (response.status, response.reason)) + if response.status != 200 or 'success' not in content: + raise Exception('Could not clear users. Recieved a response of %s %s \n %s' % (response.status, response.reason, content)) From 6e22417789a4e4b25477f6fe0bb777bd9abd43f4 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 15:16:39 -0400 Subject: [PATCH 23/53] Remove skipped decorator from tests. --- tests/client_tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/client_tests.py b/tests/client_tests.py index 10432ba..f3e612f 100644 --- a/tests/client_tests.py +++ b/tests/client_tests.py @@ -106,7 +106,6 @@ def test_make_request_not_found(self): client = self.get_client() client.make_request(path) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_post_request(self): path = 'customers/new' @@ -120,7 +119,6 @@ def test_post_request(self): client = self.get_client() client.make_request(path, data=data) - @unittest.skip('Skip until deleting customers is working') def generate_error_response(self, auxcode=None, path=None, params=None, **overrides): ''' Creates a request to cheddar which should return an error @@ -244,7 +242,6 @@ def test_format_datetime_with_now(self): self.assertEquals(expected, result) - @unittest.skip('I can not figure this out.') @clear_users def test_chedder_update_customer_error(self): """ From 77af8cc6b32313375de32b8b4248646b2332a021 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 15:18:04 -0400 Subject: [PATCH 24/53] Add unittests for __repr__ and __unicode__ on the Promotion object. Remove the skip test decorator from unit tests. Add work around for duplicate transaction failures. --- tests/product_tests.py | 84 ++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/tests/product_tests.py b/tests/product_tests.py index c0852e5..b834c8d 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -1,3 +1,6 @@ +import random +import string + from copy import copy from datetime import date, datetime, timedelta from decimal import Decimal @@ -116,6 +119,13 @@ def test_plan_initial_bill_date(self): def get_customer(self, **kwargs): customer_data = copy(self.customer_defaults) + # We need to make unique customers with the same data. + # Cheddar recomends we pass a garbage field. + # http://support.cheddargetter.com/discussions/problems/8342-duplicate-post + # http://support.cheddargetter.com/kb/api-8/error-handling#duplicate + random_string = ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.digits) for _ in range(5)) + customer_data.update({'notes': random_string}) customer_data.update(kwargs) product = self.get_product() @@ -138,72 +148,58 @@ def get_customer_with_items(self, **kwargs): return customer - @unittest.skip('Skip until deleting customers is working') @clear_users def test_simple_create_customer(self): self.get_customer() - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_company(self): self.get_customer(company='Test Co') - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_meta_data(self): self.get_customer(meta_data = {'key_1': 'value_1', 'key2': 'value_2'}) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_true_vat_exempt(self): self.get_customer(is_vat_exempt=True) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_false_vat_exempt(self): self.get_customer(is_vat_exempt=False) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_vat_number(self): self.get_customer(vat_number=12345) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_notes(self): self.get_customer(notes='This is a test note!') - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_first_contact_datetime(self): self.get_customer(first_contact_datetime=datetime.now()) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_referer(self): self.get_customer(referer='http://saaspire.com/test.html') - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_campaign_term(self): self.get_customer(campaign_term='testing') - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_campaign_name(self): self.get_customer(campaign_name='testing') - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_campaign_source(self): self.get_customer(campaign_source='testing') - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_campaign_content(self): self.get_customer(campaign_content='testing') - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_customer_with_initial_bill_date(self): initial_bill_date = datetime.utcnow() + timedelta(days=60) @@ -216,12 +212,10 @@ def test_create_customer_with_initial_bill_date(self): diff = initial_bill_date.date() - real_bill_date.date() self.assertLessEqual(diff.days, 1) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer(self): self.get_customer(**self.paid_defaults) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer_with_charges(self): data = copy(self.paid_defaults) @@ -231,7 +225,6 @@ def test_create_paid_customer_with_charges(self): data['charges'] = charges self.get_customer(**data) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer_with_decimal_charges(self): data = copy(self.paid_defaults) @@ -241,7 +234,6 @@ def test_create_paid_customer_with_decimal_charges(self): data['charges'] = charges self.get_customer(**data) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer_with_items(self): data = copy(self.paid_defaults) @@ -252,7 +244,6 @@ def test_create_paid_customer_with_items(self): data['plan_code'] = 'TRACKED_MONTHLY' self.get_customer(**data) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paid_customer_with_decimal_quantity_items(self): data = copy(self.paid_defaults) @@ -263,13 +254,11 @@ def test_create_paid_customer_with_decimal_quantity_items(self): data['plan_code'] = 'TRACKED_MONTHLY' self.get_customer(**data) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_create_paypal_customer(self): data = copy(self.paypal_defaults) self.get_customer(**data) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_update_paypal_customer(self): data = copy(self.paypal_defaults) @@ -280,7 +269,6 @@ def test_update_paypal_customer(self): cancel_url='http://example.com/update-cancel/', ) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_customer_repr(self): customer = self.get_customer() @@ -290,7 +278,6 @@ def test_customer_repr(self): self.assertEquals(expected, result) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_subscription_repr(self): customer = self.get_customer() @@ -301,7 +288,6 @@ def test_subscription_repr(self): self.assertIn(expected, result) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_pricing_plan_repr(self): customer = self.get_customer() @@ -313,7 +299,6 @@ def test_pricing_plan_repr(self): self.assertEquals(expected, result) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_item_repr(self): customer = self.get_customer_with_items() @@ -325,7 +310,6 @@ def test_item_repr(self): self.assertEquals(expected, result) - @unittest.skip('Duplicate transaction error. 6 seconds.') @clear_users def test_get_customers(self): customer1 = self.get_customer() @@ -340,10 +324,8 @@ def test_get_customers(self): product = self.get_product() fetched_customers = product.get_customers() - self.assertEquals(2, len(fetched_customers)) - @unittest.skip('Duplicate transaction error. 6 seconds.') @clear_users def test_get_customer(self): created_customer = self.get_customer() @@ -356,7 +338,6 @@ def test_get_customer(self): self.assertEquals(created_customer.last_name, fetched_customer.last_name) self.assertEquals(created_customer.email, fetched_customer.email) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_simple_customer_update(self): new_name = 'Different' @@ -369,7 +350,6 @@ def test_simple_customer_update(self): fetched_customer = product.get_customer(code=customer.code) self.assertEquals(customer.first_name, fetched_customer.first_name) - @unittest.skip('Skip until deleting customers is working') @clear_users @raises(NotFound) def test_delete_customer(self): @@ -382,7 +362,7 @@ def test_delete_customer(self): customer.delete() fetched_customer = product.get_customer(code=customer.code) - @unittest.skip('Skip until deleting customers is working') + @clear_users def test_delete_all_customers(self): customer_1 = self.get_customer() @@ -397,7 +377,6 @@ def test_delete_all_customers(self): fetched_customers = product.get_customers() self.assertEquals(0, len(fetched_customers)) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_cancel_subscription(self): customer = self.get_customer() @@ -426,22 +405,18 @@ def assert_increment(self, quantity=None): fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_simple_increment(self): self.assert_increment() - @unittest.skip('Skip until deleting customers is working') @clear_users def test_int_increment(self): self.assert_increment(1) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_float_increment(self): self.assert_increment(1.234) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_decimal_increment(self): self.assert_increment(Decimal('1.234')) @@ -462,22 +437,18 @@ def assert_decrement(self, quantity=None): fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_simple_decrement(self): self.assert_decrement() - @unittest.skip('Duplicate transaction error. 6 seconds.') @clear_users def test_int_decrement(self): self.assert_decrement(1) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_float_decrement(self): self.assert_decrement(1.234) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_decimal_decrement(self): self.assert_decrement(Decimal('1.234')) @@ -497,17 +468,14 @@ def assert_set(self, quantity): fetched_item = customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - @unittest.skip('Duplicate transaction error. 6 seconds.') @clear_users def test_int_set(self): self.assert_set(1) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_float_set(self): self.assert_set(1.234) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_decimal_set(self): self.assert_set(Decimal('1.234')) @@ -548,27 +516,22 @@ def assert_charged(self, code, each_amount, quantity=None, self.assertEqual(quantity, fetched_charge['quantity']) self.assertEqual(description, fetched_charge['description']) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_float_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=2.3, quantity=2) - - @unittest.skip('Skip until deleting customers is working') + @clear_users def test_add_decimal_charge(self): self.assert_charged(code='TEST-CHARGE', each_amount=Decimal('2.3'), quantity=3) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_charge_with_descriptions(self): self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1, description="A test charge") - @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_credit(self): self.assert_charged(code='TEST-CHARGE', each_amount=-1, quantity=1) @@ -613,7 +576,6 @@ def assertOneTimeInvoice(self, charges): invoice_type = 'one-time', ) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_simple_one_time_invoice(self): charges = [{ @@ -624,7 +586,6 @@ def test_add_simple_one_time_invoice(self): self.assertOneTimeInvoice(charges) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_one_time_invoice_with_description(self): charges = [{ @@ -636,7 +597,6 @@ def test_add_one_time_invoice_with_description(self): self.assertOneTimeInvoice(charges) - @unittest.skip('Skip until deleting customers is working') @clear_users def test_add_one_time_invoice_with_multiple_charges(self): charges = [{ @@ -673,3 +633,23 @@ def test_get_promotion(self): self.assertEqual(promotion.coupons[0].get('code'), 'COUPON') self.assertEqual(promotion.incentives[0].get('percentage'), '10') self.assertEqual(promotion.incentives[0].get('expiration_datetime'), None) + + def test_promotion_repr(self): + ''' Test the internal __repr___ method of Promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + expected = 'Promotion: Coupon (COUPON)' + result = repr(promotion) + + self.assertEquals(expected, result) + + def test_promotion_unicode(self): + ''' Test the internal __unicode___ method of Promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + expected = 'Coupon (COUPON)' + result = unicode(promotion) + + self.assertEquals(expected, result) From 038ea9a678edbcaeca433c61db5b3cea8464d9a6 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 16:41:32 -0400 Subject: [PATCH 25/53] I hate to be that guy but OCD. PEP8 fixes. --- setup.py | 2 +- sharpy/client.py | 28 +- sharpy/exceptions.py | 28 +- sharpy/parsers.py | 285 ++++---- sharpy/product.py | 514 ++++++++------- tests/client_tests.py | 98 +-- tests/parser_tests.py | 1018 ++++++++++++++++------------- tests/product_tests.py | 263 ++++---- tests/testing_tools/decorators.py | 12 +- tests/testing_tools/utils.py | 9 +- 10 files changed, 1243 insertions(+), 1014 deletions(-) diff --git a/setup.py b/setup.py index fd44c3d..ce2c96b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python try: - from setuptools import setup + from setuptools import setup except ImportError, err: from distutils.core import setup diff --git a/sharpy/client.py b/sharpy/client.py index 88f940f..980d204 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -1,17 +1,26 @@ import base64 import logging from urllib import urlencode -from decimal import getcontext from dateutil.tz import tzutc import httplib2 -from sharpy.exceptions import CheddarError, AccessDenied, BadRequest, NotFound, PreconditionFailed, CheddarFailure, NaughtyGateway, UnprocessableEntity +from sharpy.exceptions import AccessDenied +from sharpy.exceptions import BadRequest +from sharpy.exceptions import CheddarError +from sharpy.exceptions import CheddarFailure +from sharpy.exceptions import NaughtyGateway +from sharpy.exceptions import NotFound +from sharpy.exceptions import PreconditionFailed +from sharpy.exceptions import UnprocessableEntity client_log = logging.getLogger('SharpyClient') + class Client(object): default_endpoint = 'https://cheddargetter.com/xml' - def __init__(self, username, password, product_code, cache=None, timeout=None, endpoint=None): + + def __init__(self, username, password, product_code, cache=None, + timeout=None, endpoint=None): ''' username - Your cheddargetter username (probably an email address) password - Your cheddargetter password @@ -83,7 +92,8 @@ def make_request(self, path, params=None, data=None, method=None): method = 'POST' body = urlencode(data) headers = { - 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'content-type': + 'application/x-www-form-urlencoded; charset=UTF-8', } client_log.debug('Request Method: %s' % method) @@ -92,10 +102,10 @@ def make_request(self, path, params=None, data=None, method=None): # Setup http client h = httplib2.Http(cache=self.cache, timeout=self.timeout) - #h.add_credentials(self.username, self.password) - # Skip the normal http client behavior and send auth headers immediately - # to save an http request. - headers['Authorization'] = "Basic %s" % base64.standard_b64encode(self.username + ':' + self.password).strip() + # Skip the normal http client behavior and send auth headers + # immediately to save an http request. + headers['Authorization'] = "Basic %s" % base64.standard_b64encode( + self.username + ':' + self.password).strip() # Make request response, content = h.request(url, method, body=body, headers=headers) @@ -123,5 +133,3 @@ def make_request(self, path, params=None, data=None, method=None): response.content = content return response - - diff --git a/sharpy/exceptions.py b/sharpy/exceptions.py index 4841dae..ab2cf17 100644 --- a/sharpy/exceptions.py +++ b/sharpy/exceptions.py @@ -2,16 +2,16 @@ class CheddarError(Exception): "Base class for exceptions returned by cheddar" - + def __init__(self, response, content, *args, **kwargs): # Importing in method to break circular dependecy from sharpy.parsers import parse_error - + super(CheddarError, self).__init__(*args, **kwargs) error_info = parse_error(content) self.response = response self.error_info = error_info - + def __str__(self): return '%s (%s) %s - %s' % ( self.response.status, @@ -20,42 +20,50 @@ def __str__(self): self.error_info['message'], ) + class AccessDenied(CheddarError): "A request to cheddar returned a status code of 401" pass - + + class BadRequest(CheddarError): "A request to cheddar was invalid in some way" pass + class NotFound(CheddarError): "A request to chedder was made for a resource which doesn't exist" pass - + + class CheddarFailure(CheddarError): "A request to cheddar encountered an unexpected error on the cheddar side" pass - + + class PreconditionFailed(CheddarError): "A request to cheddar was made but failed CG's validation in some way." pass - + + class NaughtyGateway(CheddarError): """ Cheddar either couldn't contact the gateway or the gateway did something very unexpected. """ pass - + + class UnprocessableEntity(CheddarError): """ An error occurred during processing. Please fix the error and try again. """ pass - + + class ParseError(Exception): """ Sharpy recieved unknown output from cheddar and doesn't know what to do with it. """ - pass \ No newline at end of file + pass diff --git a/sharpy/parsers.py b/sharpy/parsers.py index 52c7aa4..d0f6f60 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -1,4 +1,3 @@ -from datetime import datetime, date from decimal import Decimal, InvalidOperation import logging @@ -8,11 +7,12 @@ from lxml.etree import XML except ImportError: from elementtree.ElementTree import XML - + from sharpy.exceptions import ParseError client_log = logging.getLogger('SharpyClient') + def parse_error(xml_str): error = {} doc = XML(xml_str) @@ -27,9 +27,9 @@ def parse_error(xml_str): error['code'] = elem.attrib['code'] error['aux_code'] = elem.attrib['auxCode'] error['message'] = elem.text - + return error - + class CheddarOutputParser(object): ''' @@ -45,9 +45,9 @@ def parse_bool(self, content): value = False else: raise ParseError("Can't parse '%s' as a bool." % content) - + return value - + def parse_int(self, content): value = None if content != '' and content is not None: @@ -55,9 +55,9 @@ def parse_int(self, content): value = int(content) except ValueError: raise ParseError("Can't parse '%s' as an int." % content) - + return value - + def parse_decimal(self, content): value = None if content != '' and content is not None: @@ -65,9 +65,9 @@ def parse_decimal(self, content): value = Decimal(content) except InvalidOperation: raise ParseError("Can't parse '%s' as a decimal." % content) - + return value - + def parse_datetime(self, content): value = None if content: @@ -75,10 +75,10 @@ def parse_datetime(self, content): value = date_parser.parse(content) except ValueError: raise ParseError("Can't parse '%s' as a datetime." % content) - - + return value - + + class PlansParser(CheddarOutputParser): ''' A utility class for parsing cheddar's xml output for pricing plans. @@ -89,9 +89,9 @@ def parse_xml(self, xml_str): for plan_xml in plans_xml: plan = self.parse_plan(plan_xml) plans.append(plan) - + return plans - + def parse_plan(self, plan_element): plan = {} plan['id'] = plan_element.attrib['id'] @@ -101,45 +101,58 @@ def parse_plan(self, plan_element): plan['is_active'] = self.parse_bool(plan_element.findtext('isActive')) plan['is_free'] = self.parse_bool(plan_element.findtext('isFree')) plan['trial_days'] = self.parse_int(plan_element.findtext('trialDays')) - plan['initial_bill_count'] = self.parse_int(plan_element.findtext('initialBillCount')) - plan['initial_bill_count_unit'] = plan_element.findtext('initialBillCountUnit') + plan['initial_bill_count'] = self.parse_int(plan_element.findtext( + 'initialBillCount')) + plan['initial_bill_count_unit'] = plan_element.findtext( + 'initialBillCountUnit') plan['billing_frequency'] = plan_element.findtext('billingFrequency') - plan['billing_frequency_per'] = plan_element.findtext('billingFrequencyPer') - plan['billing_frequency_unit'] = plan_element.findtext('billingFrequencyUnit') - plan['billing_frequency_quantity'] = self.parse_int(plan_element.findtext('billingFrequencyQuantity')) + plan['billing_frequency_per'] = plan_element.findtext( + 'billingFrequencyPer') + plan['billing_frequency_unit'] = plan_element.findtext( + 'billingFrequencyUnit') + plan['billing_frequency_quantity'] = self.parse_int( + plan_element.findtext('billingFrequencyQuantity')) plan['setup_charge_code'] = plan_element.findtext('setupChargeCode') - plan['setup_charge_amount'] = self.parse_decimal(plan_element.findtext('setupChargeAmount')) - plan['recurring_charge_code'] = plan_element.findtext('recurringChargeCode') - plan['recurring_charge_amount'] = self.parse_decimal(plan_element.findtext('recurringChargeAmount')) - plan['created_datetime'] = self.parse_datetime(plan_element.findtext('createdDatetime')) - + plan['setup_charge_amount'] = self.parse_decimal( + plan_element.findtext('setupChargeAmount')) + plan['recurring_charge_code'] = plan_element.findtext( + 'recurringChargeCode') + plan['recurring_charge_amount'] = self.parse_decimal( + plan_element.findtext('recurringChargeAmount')) + plan['created_datetime'] = self.parse_datetime( + plan_element.findtext('createdDatetime')) + plan['items'] = self.parse_plan_items(plan_element.find('items')) - + return plan - + def parse_plan_items(self, items_element): items = [] - + if items_element is not None: for item_element in items_element: items.append(self.parse_plan_item(item_element)) - + return items - + def parse_plan_item(self, item_element): item = {} - + item['id'] = item_element.attrib['id'] item['code'] = item_element.attrib['code'] item['name'] = item_element.findtext('name') - item['quantity_included'] = self.parse_decimal(item_element.findtext('quantityIncluded')) - item['is_periodic'] = self.parse_bool(item_element.findtext('isPeriodic')) - item['overage_amount'] = self.parse_decimal(item_element.findtext('overageAmount')) - item['created_datetime'] = self.parse_datetime(item_element.findtext('createdDatetime')) - + item['quantity_included'] = self.parse_decimal( + item_element.findtext('quantityIncluded')) + item['is_periodic'] = self.parse_bool( + item_element.findtext('isPeriodic')) + item['overage_amount'] = self.parse_decimal( + item_element.findtext('overageAmount')) + item['created_datetime'] = self.parse_datetime( + item_element.findtext('createdDatetime')) + return item - - + + class CustomersParser(CheddarOutputParser): ''' Utility class for parsing cheddar's xml output for customers. @@ -150,12 +163,12 @@ def parse_xml(self, xml_str): for customer_xml in customers_xml: customer = self.parse_customer(customer_xml) customers.append(customer) - + return customers - + def parse_customer(self, customer_element): customer = {} - + # Basic info customer['id'] = customer_element.attrib['id'] customer['code'] = customer_element.attrib['code'] @@ -167,60 +180,72 @@ def parse_customer(self, customer_element): customer['gateway_token'] = customer_element.findtext('gateway_token') customer['is_vat_exempt'] = customer_element.findtext('isVatExempt') customer['vat_number'] = customer_element.findtext('vatNumber') - customer['first_contact_datetime'] = self.parse_datetime(customer_element.findtext('firstContactDatetime')) + customer['first_contact_datetime'] = self.parse_datetime( + customer_element.findtext('firstContactDatetime')) customer['referer'] = customer_element.findtext('referer') customer['referer_host'] = customer_element.findtext('refererHost') - customer['campaign_source'] = customer_element.findtext('campaignSource') - customer['campaign_medium'] = customer_element.findtext('campaignMedium') + customer['campaign_source'] = customer_element.findtext( + 'campaignSource') + customer['campaign_medium'] = customer_element.findtext( + 'campaignMedium') customer['campaign_term'] = customer_element.findtext('campaignTerm') - customer['campaign_content'] = customer_element.findtext('campaignContent') + customer['campaign_content'] = customer_element.findtext( + 'campaignContent') customer['campaign_name'] = customer_element.findtext('campaignName') - customer['created_datetime'] = self.parse_datetime(customer_element.findtext('createdDatetime')) - customer['modified_datetime'] = self.parse_datetime(customer_element.findtext('modifiedDatetime')) - + customer['created_datetime'] = self.parse_datetime( + customer_element.findtext('createdDatetime')) + customer['modified_datetime'] = self.parse_datetime( + customer_element.findtext('modifiedDatetime')) + # Metadata - customer['meta_data'] = self.parse_meta_data(customer_element.find('metaData')) - + customer['meta_data'] = self.parse_meta_data( + customer_element.find('metaData')) + # Subscriptions - customer['subscriptions'] = self.parse_subscriptions(customer_element.find('subscriptions')) - + customer['subscriptions'] = self.parse_subscriptions( + customer_element.find('subscriptions')) + return customer - + def parse_meta_data(self, meta_data_element): meta_data = [] for meta_datum_element in meta_data_element: meta_data.append(self.parse_meta_datum(meta_datum_element)) - + return meta_data - + def parse_meta_datum(self, meta_datum_element): meta_datum = {} - + meta_datum['id'] = meta_datum_element.attrib['id'] meta_datum['name'] = meta_datum_element.findtext('name') meta_datum['value'] = meta_datum_element.findtext('value') - meta_datum['created_datetime'] = self.parse_datetime(meta_datum_element.findtext('createdDatetime')) - meta_datum['modified_datetime'] = self.parse_datetime(meta_datum_element.findtext('modifiedDatetime')) - + meta_datum['created_datetime'] = self.parse_datetime( + meta_datum_element.findtext('createdDatetime')) + meta_datum['modified_datetime'] = self.parse_datetime( + meta_datum_element.findtext('modifiedDatetime')) + return meta_datum - - + def parse_subscriptions(self, subscriptions_element): subscriptions = [] for subscription_element in subscriptions_element: subscription = self.parse_subscription(subscription_element) subscriptions.append(subscription) - + return subscriptions - + def parse_subscription(self, subscription_element): subscription = {} - + # Basic info subscription['id'] = subscription_element.attrib['id'] - subscription['gateway_token'] = subscription_element.findtext('gatewayToken') - subscription['cc_first_name'] = subscription_element.findtext('ccFirstName') - subscription['cc_last_name'] = subscription_element.findtext('ccLastName') + subscription['gateway_token'] = subscription_element.findtext( + 'gatewayToken') + subscription['cc_first_name'] = subscription_element.findtext( + 'ccFirstName') + subscription['cc_last_name'] = subscription_element.findtext( + 'ccLastName') subscription['cc_company'] = subscription_element.findtext('ccCompany') subscription['cc_country'] = subscription_element.findtext('ccCountry') subscription['cc_address'] = subscription_element.findtext('ccAddress') @@ -229,12 +254,18 @@ def parse_subscription(self, subscription_element): subscription['cc_zip'] = subscription_element.findtext('ccZip') subscription['cc_type'] = subscription_element.findtext('ccType') subscription['cc_email'] = subscription_element.findtext('ccEmail') - subscription['cc_last_four'] = subscription_element.findtext('ccLastFour') - subscription['cc_expiration_date'] = subscription_element.findtext('ccExpirationDate') - subscription['cancel_type'] = subscription_element.findtext('cancelType') - subscription['cancel_reason'] = subscription_element.findtext('cancelReason') - subscription['canceled_datetime'] = self.parse_datetime(subscription_element.findtext('canceledDatetime')) - subscription['created_datetime'] = self.parse_datetime(subscription_element.findtext('createdDatetime')) + subscription['cc_last_four'] = subscription_element.findtext( + 'ccLastFour') + subscription['cc_expiration_date'] = subscription_element.findtext( + 'ccExpirationDate') + subscription['cancel_type'] = subscription_element.findtext( + 'cancelType') + subscription['cancel_reason'] = subscription_element.findtext( + 'cancelReason') + subscription['canceled_datetime'] = self.parse_datetime( + subscription_element.findtext('canceledDatetime')) + subscription['created_datetime'] = self.parse_datetime( + subscription_element.findtext('createdDatetime')) gateway_account_element = subscription_element.find('gatewayAccount') if gateway_account_element is not None: subscription['gateway_account'] = { @@ -242,91 +273,105 @@ def parse_subscription(self, subscription_element): 'gateway': gateway_account_element.findtext('gateway'), 'type': gateway_account_element.findtext('type') } - subscription['redirect_url'] = subscription_element.findtext('redirectUrl') - + subscription['redirect_url'] = subscription_element.findtext( + 'redirectUrl') + # Plans - subscription['plans'] = self.parse_plans(subscription_element.find('plans')) - + subscription['plans'] = self.parse_plans( + subscription_element.find('plans')) + # Invoices - subscription['invoices'] = self.parse_invoices(subscription_element.find('invoices')) - - subscription['items'] = self.parse_subscription_items(subscription_element.find('items')) - + subscription['invoices'] = self.parse_invoices( + subscription_element.find('invoices')) + + subscription['items'] = self.parse_subscription_items( + subscription_element.find('items')) + return subscription - + def parse_plans(self, plans_element): plans_parser = PlansParser() plans = [] - + if plans_element is not None: for plan_element in plans_element: plans.append(plans_parser.parse_plan(plan_element)) - + return plans - + def parse_invoices(self, invoices_element): invoices = [] if invoices_element is not None: for invoice_element in invoices_element: invoices.append(self.parse_invoice(invoice_element)) - + return invoices - + def parse_invoice(self, invoice_element): invoice = {} - + invoice['id'] = invoice_element.attrib['id'] invoice['number'] = invoice_element.findtext('number') invoice['type'] = invoice_element.findtext('type') invoice['vat_rate'] = invoice_element.findtext('vatRate') - invoice['billing_datetime'] = self.parse_datetime(invoice_element.findtext('billingDatetime')) - invoice['paid_transaction_id'] = invoice_element.findtext('paidTransactionId') - invoice['created_datetime'] = self.parse_datetime(invoice_element.findtext('createdDatetime')) - - invoice['charges'] = self.parse_charges(invoice_element.find('charges')) - + invoice['billing_datetime'] = self.parse_datetime( + invoice_element.findtext('billingDatetime')) + invoice['paid_transaction_id'] = invoice_element.findtext( + 'paidTransactionId') + invoice['created_datetime'] = self.parse_datetime( + invoice_element.findtext('createdDatetime')) + + invoice['charges'] = self.parse_charges( + invoice_element.find('charges')) + return invoice - + def parse_charges(self, charges_element): charges = [] - + for charge_element in charges_element: charges.append(self.parse_charge(charge_element)) - + return charges - + def parse_charge(self, charge_element): charge = {} - + charge['id'] = charge_element.attrib['id'] charge['code'] = charge_element.attrib['code'] charge['type'] = charge_element.findtext('type') - charge['quantity'] = self.parse_decimal(charge_element.findtext('quantity')) - charge['each_amount'] = self.parse_decimal(charge_element.findtext('eachAmount')) + charge['quantity'] = self.parse_decimal( + charge_element.findtext('quantity')) + charge['each_amount'] = self.parse_decimal( + charge_element.findtext('eachAmount')) charge['description'] = charge_element.findtext('description') - charge['created_datetime'] = self.parse_datetime(charge_element.findtext('createdDatetime')) - + charge['created_datetime'] = self.parse_datetime( + charge_element.findtext('createdDatetime')) + return charge - + def parse_subscription_items(self, items_element): items = [] - + if items_element is not None: for item_element in items_element: items.append(self.parse_subscription_item(item_element)) - + return items - + def parse_subscription_item(self, item_element): item = {} item['id'] = item_element.attrib['id'] item['code'] = item_element.attrib['code'] item['name'] = item_element.findtext('name') - item['quantity'] = self.parse_decimal(item_element.findtext('quantity')) - item['created_datetime'] = self.parse_datetime(item_element.findtext('createdDatetime')) - item['modified_datetime'] = self.parse_datetime(item_element.findtext('modifiedDatetime')) - + item['quantity'] = self.parse_decimal( + item_element.findtext('quantity')) + item['created_datetime'] = self.parse_datetime( + item_element.findtext('createdDatetime')) + item['modified_datetime'] = self.parse_datetime( + item_element.findtext('modifiedDatetime')) + return item @@ -348,10 +393,13 @@ def parse_promotion(self, promotion_element): promotion['id'] = promotion_element.attrib['id'] promotion['name'] = promotion_element.findtext('name') promotion['description'] = promotion_element.findtext('description') - promotion['created_datetime'] = self.parse_datetime(promotion_element.findtext('createdDatetime')) + promotion['created_datetime'] = self.parse_datetime( + promotion_element.findtext('createdDatetime')) - promotion['incentives'] = self.parse_incentives(promotion_element.find('incentives')) - promotion['coupons'] = self.parse_coupons(promotion_element.find('coupons')) + promotion['incentives'] = self.parse_incentives( + promotion_element.find('incentives')) + promotion['coupons'] = self.parse_coupons( + promotion_element.find('coupons')) return promotion @@ -371,7 +419,8 @@ def parse_incentive(self, incentive_element): incentive['type'] = incentive_element.findtext('type') incentive['percentage'] = incentive_element.findtext('percentage') incentive['months'] = incentive_element.findtext('months') - incentive['created_datetime'] = self.parse_datetime(incentive_element.findtext('createdDatetime')) + incentive['created_datetime'] = self.parse_datetime( + incentive_element.findtext('createdDatetime')) return incentive @@ -390,7 +439,9 @@ def parse_coupon(self, coupon_element): coupon['id'] = coupon_element.attrib['id'] coupon['code'] = coupon_element.attrib['code'] coupon['max_redemptions'] = coupon_element.findtext('maxRedemptions') - coupon['expiration_datetime'] = self.parse_datetime(coupon_element.findtext('expirationDatetime')) - coupon['created_datetime'] = self.parse_datetime(coupon_element.findtext('createdDatetime')) + coupon['expiration_datetime'] = self.parse_datetime( + coupon_element.findtext('expirationDatetime')) + coupon['created_datetime'] = self.parse_datetime( + coupon_element.findtext('createdDatetime')) return coupon diff --git a/sharpy/product.py b/sharpy/product.py index 817260d..a2e2414 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -1,6 +1,6 @@ from copy import copy -from datetime import date, datetime -from decimal import Decimal, getcontext as get_decimal_context +from datetime import datetime +from decimal import Decimal from time import time from dateutil.relativedelta import relativedelta @@ -9,9 +9,11 @@ from sharpy.exceptions import NotFound from sharpy.parsers import PlansParser, CustomersParser, PromotionsParser + class CheddarProduct(object): - - def __init__(self, username, password, product_code, cache=None, timeout=None, endpoint=None): + + def __init__(self, username, password, product_code, cache=None, + timeout=None, endpoint=None): self.product_code = product_code self.client = Client( username, @@ -21,20 +23,20 @@ def __init__(self, username, password, product_code, cache=None, timeout=None, e timeout, endpoint, ) - + super(CheddarProduct, self).__init__() - + def __repr__(self): return u'CheddarProduct: %s' % self.product_code - + def get_all_plans(self): response = self.client.make_request(path='plans/get') plans_parser = PlansParser() plans_data = plans_parser.parse_xml(response.content) plans = [PricingPlan(**plan_data) for plan_data in plans_data] - + return plans - + def get_plan(self, code): response = self.client.make_request( path='plans/get', @@ -43,83 +45,93 @@ def get_plan(self, code): plans_parser = PlansParser() plans_data = plans_parser.parse_xml(response.content) plans = [PricingPlan(**plan_data) for plan_data in plans_data] - + return plans[0] - - def create_customer(self, code, first_name, last_name, email, plan_code, \ - company=None, is_vat_exempt=None, vat_number=None, \ - notes=None, first_contact_datetime=None, \ - referer=None, campaign_term=None, \ - campaign_name=None, campaign_source=None, \ - campaign_medium=None, campaign_content=None, \ - meta_data=None, initial_bill_date=None, method=None,\ - cc_number=None, cc_expiration=None, \ - cc_card_code=None, cc_first_name=None, \ - cc_last_name=None, cc_email=None, cc_company=None, \ - cc_country=None, cc_address=None, cc_city=None, \ - cc_state=None, cc_zip=None, return_url=None, \ + + def create_customer(self, code, first_name, last_name, email, plan_code, + company=None, is_vat_exempt=None, vat_number=None, + notes=None, first_contact_datetime=None, + referer=None, campaign_term=None, + campaign_name=None, campaign_source=None, + campaign_medium=None, campaign_content=None, + meta_data=None, initial_bill_date=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_email=None, cc_company=None, + cc_country=None, cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, return_url=None, cancel_url=None, charges=None, items=None): - data = self.build_customer_post_data(code, first_name, last_name, \ - email, plan_code, company, is_vat_exempt, vat_number, \ - notes, first_contact_datetime, referer, campaign_term, \ - campaign_name, campaign_source, campaign_medium, \ - campaign_content, meta_data, initial_bill_date, method, \ - cc_number, cc_expiration, cc_card_code, cc_first_name, \ - cc_last_name, cc_email, cc_company, cc_country, cc_address, \ - cc_city, cc_state, cc_zip, return_url, cancel_url) - + data = self.build_customer_post_data(code, first_name, last_name, + email, plan_code, company, + is_vat_exempt, vat_number, + notes, first_contact_datetime, + referer, campaign_term, + campaign_name, campaign_source, + campaign_medium, campaign_content, + meta_data, initial_bill_date, + method, cc_number, cc_expiration, + cc_card_code, cc_first_name, + cc_last_name, cc_email, + cc_company, cc_country, + cc_address, cc_city, cc_state, + cc_zip, return_url, cancel_url) + if charges: for i, charge in enumerate(charges): data['charges[%d][chargeCode]' % i] = charge['code'] data['charges[%d][quantity]' % i] = charge.get('quantity', 1) - data['charges[%d][eachAmount]' % i] = '%.2f' % charge['each_amount'] - data['charges[%d][description]' % i] = charge.get('description', '') + data['charges[%d][eachAmount]' % i] = '%.2f' % ( + charge['each_amount']) + data['charges[%d][description]' % i] = charge.get( + 'description', '') if items: for i, item in enumerate(items): data['items[%d][itemCode]' % i] = item['code'] data['items[%d][quantity]' % i] = item.get('quantity', 1) - + response = self.client.make_request(path='customers/new', data=data) customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(response.content) customer = Customer(product=self, **customers_data[0]) - + return customer - - def build_customer_post_data(self, code=None, first_name=None,\ - last_name=None, email=None, plan_code=None, \ - company=None, is_vat_exempt=None, vat_number=None, \ - notes=None, first_contact_datetime=None, \ - referer=None, campaign_term=None, \ - campaign_name=None, campaign_source=None, \ - campaign_medium=None, campaign_content=None, \ - meta_data=None, initial_bill_date=None, method=None,\ - cc_number=None, cc_expiration=None, \ - cc_card_code=None, cc_first_name=None, \ - cc_last_name=None, cc_email=None, cc_company=None, \ - cc_country=None, cc_address=None, cc_city=None, \ - cc_state=None, cc_zip=None, return_url=None, cancel_url=None, \ - bill_date=None): + + def build_customer_post_data(self, code=None, first_name=None, + last_name=None, email=None, plan_code=None, + company=None, is_vat_exempt=None, + vat_number=None, notes=None, + first_contact_datetime=None, referer=None, + campaign_term=None, campaign_name=None, + campaign_source=None, campaign_medium=None, + campaign_content=None, meta_data=None, + initial_bill_date=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_email=None, + cc_company=None, cc_country=None, + cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, return_url=None, + cancel_url=None, bill_date=None): data = {} - + if code: data['code'] = code - + if first_name: data['firstName'] = first_name - + if last_name: data['lastName'] = last_name - + if email: data['email'] = email - + if plan_code: data['subscription[planCode]'] = plan_code - + if company: data['company'] = company @@ -136,7 +148,8 @@ def build_customer_post_data(self, code=None, first_name=None,\ data['notes'] = notes if first_contact_datetime: - data['firstContactDatetime'] = self.client.format_datetime(first_contact_datetime) + data['firstContactDatetime'] = self.client.format_datetime( + first_contact_datetime) if referer: data['referer'] = referer @@ -159,8 +172,9 @@ def build_customer_post_data(self, code=None, first_name=None,\ data[full_key] = value if initial_bill_date: - data['subscription[initialBillDate]'] = self.client.format_date(initial_bill_date) - + data['subscription[initialBillDate]'] = self.client.format_date( + initial_bill_date) + if method: data['subscription[method]'] = method @@ -202,19 +216,20 @@ def build_customer_post_data(self, code=None, first_name=None,\ if return_url: data['subscription[returnUrl]'] = return_url - + if cancel_url: data['subscription[cancelUrl]'] = cancel_url - + if bill_date: - data['subscription[changeBillDate]'] = self.client.format_datetime(bill_date) + data['subscription[changeBillDate]'] = self.client.format_datetime( + bill_date) return data - + def get_customers(self, filter_data=None): ''' - Returns all customers. Sometimes they are too much and cause internal - server errors on CG. API call permits post parameters for filtering + Returns all customers. Sometimes they are too much and cause internal + server errors on CG. API call permits post parameters for filtering which tends to fix this https://cheddargetter.com/developers#all-customers @@ -226,43 +241,45 @@ def get_customers(self, filter_data=None): ] ''' customers = [] - + try: - response = self.client.make_request(path='customers/get', data=filter_data) + response = self.client.make_request(path='customers/get', + data=filter_data) except NotFound: response = None - + if response: customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(response.content) for customer_data in customers_data: customers.append(Customer(product=self, **customer_data)) - + return customers - + def get_customer(self, code): - + response = self.client.make_request( path='customers/get', params={'code': code}, ) customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(response.content) - + return Customer(product=self, **customers_data[0]) - + def delete_all_customers(self): ''' This method does exactly what you think it does. Calling this method deletes all customer data in your cheddar product and the configured gateway. This action cannot be undone. - + DO NOT RUN THIS UNLESS YOU REALLY, REALLY, REALLY MEAN TO! ''' response = self.client.make_request( path='customers/delete-all/confirm/%d' % int(time()), method='POST' ) + return self.load_data_from_xml(response.content) def get_all_promotions(self): ''' @@ -300,41 +317,41 @@ def get_promotion(self, code): class PricingPlan(object): - + def __init__(self, name, code, id, description, is_active, is_free, - trial_days, initial_bill_count, initial_bill_count_unit, + trial_days, initial_bill_count, initial_bill_count_unit, billing_frequency, billing_frequency_per, billing_frequency_quantity, billing_frequency_unit, setup_charge_code, setup_charge_amount, recurring_charge_code, recurring_charge_amount, created_datetime, items, subscription=None): - + self.load_data(name=name, code=code, id=id, description=description, - is_active=is_active, is_free=is_free, - trial_days=trial_days, - initial_bill_count=initial_bill_count, - initial_bill_count_unit=initial_bill_count_unit, - billing_frequency=billing_frequency, - billing_frequency_per=billing_frequency_per, - billing_frequency_quantity=billing_frequency_quantity, - billing_frequency_unit=billing_frequency_unit, - setup_charge_code=setup_charge_code, - setup_charge_amount=setup_charge_amount, - recurring_charge_code=recurring_charge_code, - recurring_charge_amount=recurring_charge_amount, - created_datetime=created_datetime, items=items, - subscription=subscription) - + is_active=is_active, is_free=is_free, + trial_days=trial_days, + initial_bill_count=initial_bill_count, + initial_bill_count_unit=initial_bill_count_unit, + billing_frequency=billing_frequency, + billing_frequency_per=billing_frequency_per, + billing_frequency_quantity=billing_frequency_quantity, + billing_frequency_unit=billing_frequency_unit, + setup_charge_code=setup_charge_code, + setup_charge_amount=setup_charge_amount, + recurring_charge_code=recurring_charge_code, + recurring_charge_amount=recurring_charge_amount, + created_datetime=created_datetime, items=items, + subscription=subscription) + super(PricingPlan, self).__init__() - + def load_data(self, name, code, id, description, is_active, is_free, - trial_days, initial_bill_count, initial_bill_count_unit, - billing_frequency, billing_frequency_per, - billing_frequency_quantity, billing_frequency_unit, - setup_charge_code, setup_charge_amount, - recurring_charge_code, recurring_charge_amount, - created_datetime, items, subscription=None): - + trial_days, initial_bill_count, initial_bill_count_unit, + billing_frequency, billing_frequency_per, + billing_frequency_quantity, billing_frequency_unit, + setup_charge_code, setup_charge_amount, + recurring_charge_code, recurring_charge_amount, + created_datetime, items, subscription=None): + self.name = name self.code = code self.id = id @@ -357,10 +374,10 @@ def load_data(self, name, code, id, description, is_active, is_free, if subscription: self.subscription = subscription - + def __repr__(self): return u'PricingPlan: %s (%s)' % (self.name, self.code) - + @property def initial_bill_date(self): ''' @@ -368,30 +385,29 @@ def initial_bill_date(self): based on available plan info. ''' time_to_start = None - + if self.initial_bill_count_unit == 'months': time_to_start = relativedelta(months=self.initial_bill_count) else: time_to_start = relativedelta(days=self.initial_bill_count) - + initial_bill_date = datetime.utcnow().date() + time_to_start - + return initial_bill_date - - + class Customer(object): - - def __init__(self, code, first_name, last_name, email, product, id=None, \ - company=None, notes=None, gateway_token=None, \ - is_vat_exempt=None, vat_number=None, \ - first_contact_datetime=None, referer=None, \ - referer_host=None, campaign_source=None, \ - campaign_medium=None, campaign_term=None, \ - campaign_content=None, campaign_name=None, \ - created_datetime=None, modified_datetime=None, \ + + def __init__(self, code, first_name, last_name, email, product, id=None, + company=None, notes=None, gateway_token=None, + is_vat_exempt=None, vat_number=None, + first_contact_datetime=None, referer=None, + referer_host=None, campaign_source=None, + campaign_medium=None, campaign_term=None, + campaign_content=None, campaign_name=None, + created_datetime=None, modified_datetime=None, meta_data=None, subscriptions=None): - + self.load_data(code=code, first_name=first_name, last_name=last_name, email=email, product=product, id=id, @@ -409,19 +425,18 @@ def __init__(self, code, first_name, last_name, email, product, id=None, \ created_datetime=created_datetime, modified_datetime=modified_datetime, meta_data=meta_data, - subscriptions=subscriptions - ) - + subscriptions=subscriptions) + super(Customer, self).__init__() - - def load_data(self, code, first_name, last_name, email, product, id=None,\ - company=None, notes=None, gateway_token=None, \ - is_vat_exempt=None, vat_number=None, \ - first_contact_datetime=None, referer=None, \ - referer_host=None, campaign_source=None, \ - campaign_medium=None, campaign_term=None, \ - campaign_content=None, campaign_name=None, \ - created_datetime=None, modified_datetime=None, \ + + def load_data(self, code, first_name, last_name, email, product, id=None, + company=None, notes=None, gateway_token=None, + is_vat_exempt=None, vat_number=None, + first_contact_datetime=None, referer=None, + referer_host=None, campaign_source=None, + campaign_medium=None, campaign_term=None, + campaign_content=None, campaign_name=None, + created_datetime=None, modified_datetime=None, meta_data=None, subscriptions=None): self.code = code self.id = id @@ -446,78 +461,73 @@ def load_data(self, code, first_name, last_name, email, product, id=None,\ self.meta_data = {} if meta_data: - for datum in meta_data: - self.meta_data[datum['name']] = datum['value'] + for datum in meta_data: + self.meta_data[datum['name']] = datum['value'] subscription_data = subscriptions[0] subscription_data['customer'] = self if hasattr(self, 'subscription'): self.subscription.load_data(**subscription_data) else: self.subscription = Subscription(**subscription_data) - + def load_data_from_xml(self, xml): customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(xml) customer_data = customers_data[0] self.load_data(product=self.product, **customer_data) - - def update(self, first_name=None, last_name=None, email=None, \ - company=None, is_vat_exempt=None, vat_number=None, \ - notes=None, first_contact_datetime=None, \ - referer=None, campaign_term=None, \ - campaign_name=None, campaign_source=None, \ - campaign_medium=None, campaign_content=None, \ - meta_data=None, method=None, \ - cc_number=None, cc_expiration=None, \ - cc_card_code=None, cc_first_name=None, \ - cc_last_name=None, cc_company=None, cc_email=None,\ - cc_country=None, cc_address=None, cc_city=None, \ - cc_state=None, cc_zip=None, plan_code=None, bill_date=None, - return_url=None, cancel_url=None,): - - data = self.product.build_customer_post_data( first_name=first_name, - last_name=last_name, email=email, plan_code=plan_code, - company=company, is_vat_exempt=is_vat_exempt, - vat_number=vat_number, notes=notes, referer=referer, - campaign_term=campaign_term, - campaign_name=campaign_name, - campaign_source=campaign_source, - campaign_medium=campaign_medium, - campaign_content=campaign_content, - meta_data=meta_data, method=method, - cc_number=cc_number, cc_expiration=cc_expiration, - cc_card_code=cc_card_code, - cc_first_name=cc_first_name, - cc_last_name=cc_last_name, cc_company=cc_company, - cc_email=cc_email, cc_country=cc_country, - cc_address=cc_address, cc_city=cc_city, - cc_state=cc_state, cc_zip=cc_zip, bill_date=bill_date, - return_url=return_url, cancel_url=cancel_url,) - + + def update(self, first_name=None, last_name=None, email=None, + company=None, is_vat_exempt=None, vat_number=None, + notes=None, first_contact_datetime=None, + referer=None, campaign_term=None, campaign_name=None, + campaign_source=None, campaign_medium=None, + campaign_content=None, meta_data=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_company=None, cc_email=None, + cc_country=None, cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, plan_code=None, bill_date=None, + return_url=None, cancel_url=None,): + + data = self.product.build_customer_post_data( + first_name=first_name, last_name=last_name, email=email, + plan_code=plan_code, company=company, is_vat_exempt=is_vat_exempt, + vat_number=vat_number, notes=notes, referer=referer, + campaign_term=campaign_term, campaign_name=campaign_name, + campaign_source=campaign_source, campaign_medium=campaign_medium, + campaign_content=campaign_content, meta_data=meta_data, + method=method, cc_number=cc_number, cc_expiration=cc_expiration, + cc_card_code=cc_card_code, + cc_first_name=cc_first_name, cc_last_name=cc_last_name, + cc_company=cc_company, cc_email=cc_email, cc_country=cc_country, + cc_address=cc_address, cc_city=cc_city, cc_state=cc_state, + cc_zip=cc_zip, bill_date=bill_date, return_url=return_url, + cancel_url=cancel_url,) + path = 'customers/edit' params = {'code': self.code} - + response = self.product.client.make_request( - path = path, - params = params, - data = data, + path=path, + params=params, + data=data, ) return self.load_data_from_xml(response.content) - - + def delete(self): path = 'customers/delete' params = {'code': self.code} response = self.product.client.make_request( - path = path, - params = params, + path=path, + params=params, ) - + return self.load_data_from_xml(response.content) + def charge(self, code, each_amount, quantity=1, description=None): ''' Add an arbitrary charge or credit to a customer's account. A positive number will create a charge. A negative number will create a credit. - + each_amount is normalized to a Decimal with a precision of 2 as that is the level of precision which the cheddar API supports. ''' @@ -530,7 +540,7 @@ def charge(self, code, each_amount, quantity=1, description=None): } if description: data['description'] = description - + response = self.product.client.make_request( path='customers/add-charge', params={'code': self.code}, @@ -541,14 +551,14 @@ def charge(self, code, each_amount, quantity=1, description=None): def create_one_time_invoice(self, charges): ''' Charges should be a list of charges to execute immediately. Each - value in the charges diectionary should be a dictionary with the + value in the charges diectionary should be a dictionary with the following keys: code - Your code for this charge. This code will be displayed in the + Your code for this charge. This code will be displayed in the user's invoice and is limited to 36 characters. quantity - A positive integer quantity. If not provided this value will + A positive integer quantity. If not provided this value will default to 1. each_amount Positive or negative integer or decimal with two digit precision. @@ -562,7 +572,7 @@ def create_one_time_invoice(self, charges): for n, charge in enumerate(charges): each_amount = Decimal(charge['each_amount']) each_amount = each_amount.quantize(Decimal('.01')) - data['charges[%d][chargeCode]' % n ] = charge['code'] + data['charges[%d][chargeCode]' % n] = charge['code'] data['charges[%d][quantity]' % n] = charge.get('quantity', 1) data['charges[%d][eachAmount]' % n] = '%.2f' % each_amount if 'description' in charge.keys(): @@ -574,49 +584,50 @@ def create_one_time_invoice(self, charges): data=data, ) return self.load_data_from_xml(response.content) - + def __repr__(self): return u'Customer: %s %s (%s)' % ( self.first_name, self.last_name, self.code ) - + + class Subscription(object): - + def __init__(self, id, gateway_token, cc_first_name, cc_last_name, cc_company, cc_country, cc_address, cc_city, cc_state, cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, - canceled_datetime=None ,created_datetime=None, + canceled_datetime=None, created_datetime=None, plans=None, invoices=None, items=None, gateway_account=None, cancel_reason=None, cancel_type=None, cc_email=None, redirect_url=None): - - self.load_data(id=id, gateway_token=gateway_token, - cc_first_name=cc_first_name, - cc_last_name=cc_last_name, - cc_company=cc_company, cc_country=cc_country, - cc_address=cc_address, cc_city=cc_city, - cc_state=cc_state, cc_zip=cc_zip, cc_type=cc_type, - cc_last_four=cc_last_four, - cc_expiration_date=cc_expiration_date, cc_email=cc_email, - customer=customer, - canceled_datetime=canceled_datetime, - created_datetime=created_datetime, plans=plans, - invoices=invoices, items=items, - gateway_account=gateway_account, - cancel_reason=cancel_reason, cancel_type=cancel_type, - redirect_url=redirect_url) - + + self.load_data(id=id, gateway_token=gateway_token, + cc_first_name=cc_first_name, + cc_last_name=cc_last_name, + cc_company=cc_company, cc_country=cc_country, + cc_address=cc_address, cc_city=cc_city, + cc_state=cc_state, cc_zip=cc_zip, cc_type=cc_type, + cc_last_four=cc_last_four, + cc_expiration_date=cc_expiration_date, + cc_email=cc_email, customer=customer, + canceled_datetime=canceled_datetime, + created_datetime=created_datetime, plans=plans, + invoices=invoices, items=items, + gateway_account=gateway_account, + cancel_reason=cancel_reason, cancel_type=cancel_type, + redirect_url=redirect_url) + super(Subscription, self).__init__() - + def load_data(self, id, gateway_token, cc_first_name, cc_last_name, cc_company, cc_country, cc_address, cc_city, cc_state, cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, - cc_email=None, canceled_datetime=None ,created_datetime=None, + cc_email=None, canceled_datetime=None, created_datetime=None, plans=None, invoices=None, items=None, gateway_account=None, cancel_reason=None, cancel_type=None, redirect_url=None): - + self.id = id self.gateway_token = gateway_token self.cc_first_name = cc_first_name @@ -647,7 +658,7 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, plan_data = plans[0] for item in plan_data['items']: items_map[item['code']]['plan_data'] = item - + if not hasattr(self, 'items'): self.items = {} for code, item_map in items_map.iteritems(): @@ -656,7 +667,7 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, item_data = copy(plan_item_data) item_data.update(subscription_item_data) item_data['subscription'] = self - + if code in self.items.keys(): item = self.items[code] item.load_data(**item_data) @@ -668,17 +679,17 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, self.plan.load_data(**plan_data) else: self.plan = PricingPlan(**plan_data) - + def __repr__(self): return u'Subscription: %s' % self.id - + def cancel(self): client = self.customer.product.client response = client.make_request( path='customers/cancel', params={'code': self.customer.code}, ) - + customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(response.content) customer_data = customers_data[0] @@ -686,27 +697,28 @@ def cancel(self): product=self.customer.product, **customer_data ) - + + class Item(object): - + def __init__(self, code, subscription, id=None, name=None, quantity_included=None, is_periodic=None, overage_amount=None, created_datetime=None, modified_datetime=None, quantity=None): - + self.load_data(code=code, subscription=subscription, id=id, name=name, - quantity_included=quantity_included, - is_periodic=is_periodic, overage_amount=overage_amount, - created_datetime=created_datetime, - modified_datetime=modified_datetime, quantity=quantity) - + quantity_included=quantity_included, + is_periodic=is_periodic, overage_amount=overage_amount, + created_datetime=created_datetime, + modified_datetime=modified_datetime, quantity=quantity) + super(Item, self).__init__() - + def load_data(self, code, subscription, id=None, name=None, - quantity_included=None, is_periodic=None, - overage_amount=None, created_datetime=None, - modified_datetime=None, quantity=None): - + quantity_included=None, is_periodic=None, + overage_amount=None, created_datetime=None, + modified_datetime=None, quantity=None): + self.code = code self.subscription = subscription self.id = id @@ -717,86 +729,86 @@ def load_data(self, code, subscription, id=None, name=None, self.overage_amount = overage_amount self.created = created_datetime self.modified = modified_datetime - + def __repr__(self): return u'Item: %s for %s' % ( self.code, self.subscription.customer.code, ) - + def _normalize_quantity(self, quantity=None): if quantity is not None: quantity = Decimal(quantity) quantity = quantity.quantize(Decimal('.0001')) - + return quantity - + def increment(self, quantity=None): ''' Increment the item's quantity by the passed in amount. If nothing is passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of + in, it is rounded to the 4th decimal place as that is the level of precision which the Cheddar API accepts. ''' data = {} if quantity: data['quantity'] = self._normalize_quantity(quantity) - + response = self.subscription.customer.product.client.make_request( - path = 'customers/add-item-quantity', - params = { + path='customers/add-item-quantity', + params={ 'code': self.subscription.customer.code, 'itemCode': self.code, }, - data = data, - method = 'POST', + data=data, + method='POST', ) - + return self.subscription.customer.load_data_from_xml(response.content) - + def decrement(self, quantity=None): ''' Decrement the item's quantity by the passed in amount. If nothing is passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of + in, it is rounded to the 4th decimal place as that is the level of precision which the Cheddar API accepts. ''' data = {} if quantity: data['quantity'] = self._normalize_quantity(quantity) - + response = self.subscription.customer.product.client.make_request( - path = 'customers/remove-item-quantity', - params = { + path='customers/remove-item-quantity', + params={ 'code': self.subscription.customer.code, 'itemCode': self.code, }, - data = data, - method = 'POST', + data=data, + method='POST', ) - + return self.subscription.customer.load_data_from_xml(response.content) - + def set(self, quantity): ''' Set the item's quantity to the passed in amount. If nothing is passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of + in, it is rounded to the 4th decimal place as that is the level of precision which the Cheddar API accepts. ''' data = {} data['quantity'] = self._normalize_quantity(quantity) - + response = self.subscription.customer.product.client.make_request( - path = 'customers/set-item-quantity', - params = { + path='customers/set-item-quantity', + params={ 'code': self.subscription.customer.code, 'itemCode': self.code, }, - data = data, - method = 'POST', + data=data, + method='POST', ) - + return self.subscription.customer.load_data_from_xml(response.content) diff --git a/tests/client_tests.py b/tests/client_tests.py index f3e612f..4841b3b 100644 --- a/tests/client_tests.py +++ b/tests/client_tests.py @@ -1,6 +1,5 @@ from copy import copy from datetime import date, timedelta, datetime -from decimal import Decimal import unittest from dateutil.tz import tzoffset @@ -9,46 +8,53 @@ from testconfig import config from sharpy.client import Client -from sharpy.exceptions import AccessDenied, BadRequest, NotFound, CheddarFailure, NaughtyGateway, UnprocessableEntity, PreconditionFailed +from sharpy.exceptions import AccessDenied +from sharpy.exceptions import BadRequest +from sharpy.exceptions import CheddarFailure +from sharpy.exceptions import NaughtyGateway +from sharpy.exceptions import NotFound +from sharpy.exceptions import PreconditionFailed +from sharpy.exceptions import UnprocessableEntity from testing_tools.decorators import clear_users + class ClientTests(unittest.TestCase): - client_defaults = { + client_defaults = { 'username': config['cheddar']['username'], 'password': config['cheddar']['password'], 'product_code': config['cheddar']['product_code'], 'endpoint': config['cheddar']['endpoint'], } - + def get_client(self, **kwargs): client_kwargs = copy(self.client_defaults) client_kwargs.update(kwargs) - + c = Client(**client_kwargs) - + return c - + def try_client(self, **kwargs): args = copy(self.client_defaults) args.update(kwargs) client = self.get_client(**kwargs) - + self.assertEquals(args['username'], client.username) - self.assertEquals(self.client_defaults['password'] ,client.password) - self.assertEquals(self.client_defaults['product_code'], client.product_code) + self.assertEquals(self.client_defaults['password'], client.password) + self.assertEquals(self.client_defaults['product_code'], + client.product_code) if 'endpoint' in args.keys(): self.assertEquals(args['endpoint'], client.endpoint) else: self.assertEquals(client.default_endpoint, client.endpoint) - + def test_basic_init(self): self.try_client() - + def test_custom_endpoint_init(self): - self.try_client(endpoint = 'http://cheddar-test.saaspire.com') - - + self.try_client(endpoint='http://cheddar-test.saaspire.com') + def try_url_build(self, path, params=None): c = self.get_client() expected = u'%s/%s/productCode/%s' % ( @@ -63,43 +69,42 @@ def try_url_build(self, path, params=None): result = c.build_url(path=path, params=params) self.assertEquals(expected, result) - + def test_basic_build_url(self): path = 'users' self.try_url_build(path) - - + def test_single_param_build_url(self): path = 'users' params = {'key': 'value'} self.try_url_build(path, params) - + def test_multi_param_build_url(self): path = 'users' params = {'key1': 'value1', 'key2': 'value2'} self.try_url_build(path, params) - + def test_make_request(self): path = 'plans/get' client = self.get_client() response = client.make_request(path) - + self.assertEquals(response.status, 200) - + @raises(AccessDenied) def test_make_request_access_denied(self): path = 'plans/get' bad_username = self.client_defaults['username'] + '_bad' client = self.get_client(username=bad_username) client.make_request(path) - + @raises(NotFound) def test_make_request_bad_request(self): """ Attempt to grab the plans without adding /get to the url. """ path = 'plans' client = self.get_client() client.make_request(path) - + @raises(NotFound) def test_make_request_not_found(self): path = 'things-which-dont-exist' @@ -118,8 +123,9 @@ def test_post_request(self): } client = self.get_client() client.make_request(path, data=data) - - def generate_error_response(self, auxcode=None, path=None, params=None, **overrides): + + def generate_error_response(self, auxcode=None, path=None, params=None, + **overrides): ''' Creates a request to cheddar which should return an error with the provided aux code. See the urls below for details @@ -135,7 +141,7 @@ def generate_error_response(self, auxcode=None, path=None, params=None, **overri zip_code = '0%d' % auxcode else: zip_code = '12345' - + data = { 'code': 'post_test', 'firstName': 'post', @@ -150,11 +156,12 @@ def generate_error_response(self, auxcode=None, path=None, params=None, **overri 'subscription[ccZip]': zip_code, } data.update(overrides) - + client = self.get_client() client.make_request(path, params=params, data=data) - - def assertCheddarError(self, auxcode, expected_exception, path=None, params=None): + + def assertCheddarError(self, auxcode, expected_exception, path=None, + params=None): assert_raises( expected_exception, self.generate_error_response, @@ -162,72 +169,75 @@ def assertCheddarError(self, auxcode, expected_exception, path=None, params=None path=path, params=params, ) - + def assertCheddarErrorForAuxCodes(self, auxcodes, expected_exception): for auxcode in auxcodes: self.assertCheddarError(auxcode, expected_exception) - + @clear_users def test_cheddar_500s(self): auxcodes = (1000, 1002, 1003) expected_exception = CheddarFailure self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) - + @clear_users def test_cheddar_400(self): ''' The cheddar docs at http://support.cheddargetter.com/kb/api-8/error-handling - say that this aux code should return a 502 but in practice + say that this aux code should return a 502 but in practice the API returns a 400. Not sure if this is a bug or typo in the docs but for now we're assuming the API is correct. ''' self.assertCheddarError(auxcode=1001, expected_exception=BadRequest) - + @clear_users def test_cheddar_401s(self): auxcodes = (2000, 2001, 2002, 2003) expected_exception = AccessDenied self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) - + @clear_users def test_cheddar_502s(self): auxcodes = (3000, 4000) expected_exception = NaughtyGateway self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) - + @clear_users def test_cheddar_422s(self): auxcodes = (5000, 5001, 5002, 5003, 6000, 6001, 6002, 7000) expected_exception = UnprocessableEntity self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) - + @clear_users @raises(PreconditionFailed) def test_cheddar_412s(self): self.generate_error_response(auxcode=2345, firstName='') - + def test_format_datetime_with_datetime(self): client = self.get_client() - result = client.format_datetime(datetime(year=2010,month=9,day=19,hour=20,minute=10,second=39)) + result = client.format_datetime(datetime( + year=2010, month=9, day=19, hour=20, minute=10, second=39)) expected = '2010-09-19T20:10:39+00:00' self.assertEquals(expected, result) def test_format_datetime_with_datetime_with_tz(self): client = self.get_client() - result = client.format_datetime(datetime(year=2010,month=9,day=19,hour=20,minute=10,second=39, tzinfo=tzoffset("BRST", -10800))) + result = client.format_datetime(datetime( + year=2010, month=9, day=19, hour=20, minute=10, second=39, + tzinfo=tzoffset("BRST", -10800))) expected = '2010-09-19T23:10:39+00:00' self.assertEquals(expected, result) def test_format_datetime_with_date(self): client = self.get_client() - result = client.format_datetime(date(year=2010,month=9,day=19)) + result = client.format_datetime(date(year=2010, month=9, day=19)) expected = '2010-09-19T00:00:00+00:00' self.assertEquals(expected, result) - + def test_format_date_with_now(self): client = self.get_client() result = client.format_date('now') @@ -257,4 +267,4 @@ def test_chedder_update_customer_error(self): expected_exception=UnprocessableEntity, path='customers/edit', params={'code': 'post_test'} - ) \ No newline at end of file + ) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 70835f7..ecfd15c 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -7,40 +7,43 @@ from nose.tools import raises from sharpy.exceptions import ParseError -from sharpy.parsers import CheddarOutputParser, parse_error, PlansParser, CustomersParser +from sharpy.parsers import CheddarOutputParser +from sharpy.parsers import parse_error +from sharpy.parsers import PlansParser +from sharpy.parsers import CustomersParser + class ParserTests(unittest.TestCase): - + def load_file(self, filename): path = os.path.join(os.path.dirname(__file__), 'files', filename) f = open(path) content = f.read() f.close() return content - - + def test_bool_parsing_true(self): parser = CheddarOutputParser() - + expected = True result = parser.parse_bool('1') - + self.assertEquals(expected, result) - + def test_bool_parsing_false(self): parser = CheddarOutputParser() - + expected = False result = parser.parse_bool('0') - + self.assertEquals(expected, result) - + @raises(ParseError) def test_bool_parsing_error(self): parser = CheddarOutputParser() - + parser.parse_bool('test') - + def test_bool_parsing_empty(self): parser = CheddarOutputParser() @@ -48,21 +51,21 @@ def test_bool_parsing_empty(self): result = parser.parse_bool('') self.assertEquals(expected, result) - + def test_int_parsing(self): parser = CheddarOutputParser() - + expected = 234 result = parser.parse_int('234') - + self.assertEquals(expected, result) - + @raises(ParseError) def test_int_parsing_error(self): parser = CheddarOutputParser() - + parser.parse_int('test') - + def test_int_parsing_empty(self): parser = CheddarOutputParser() @@ -70,21 +73,21 @@ def test_int_parsing_empty(self): result = parser.parse_int('') self.assertEquals(expected, result) - + def test_decimal_parsing(self): parser = CheddarOutputParser() - + expected = Decimal('2.345') result = parser.parse_decimal('2.345') - + self.assertEquals(expected, result) - + @raises(ParseError) def test_decimal_parsing_error(self): parser = CheddarOutputParser() - + parser.parse_decimal('test') - + def test_decimal_parsing_empty(self): parser = CheddarOutputParser() @@ -92,10 +95,10 @@ def test_decimal_parsing_empty(self): result = parser.parse_decimal('') self.assertEquals(expected, result) - + def test_datetime_parsing(self): parser = CheddarOutputParser() - + expected = datetime( year=2011, month=1, @@ -106,27 +109,26 @@ def test_datetime_parsing(self): tzinfo=tzutc(), ) result = parser.parse_datetime('2011-01-07T20:46:43+00:00') - + self.assertEquals(expected, result) - + @raises(ParseError) def test_datetime_parsing_error(self): parser = CheddarOutputParser() - + parser.parse_datetime('test') - + def test_datetime_parsing_empty(self): parser = CheddarOutputParser() - + expected = None result = parser.parse_datetime('') - + self.assertEquals(expected, result) - - + def test_error_parser(self): error_xml = self.load_file('error.xml') - + expected = { 'aux_code': '', 'code': '400', @@ -134,19 +136,22 @@ def test_error_parser(self): 'message': 'No product selected. Need a productId or productCode.', } result = parse_error(error_xml) - + self.assertEquals(expected, result) - + def test_plans_parser(self): plans_xml = self.load_file('plans.xml') parser = PlansParser() - - expected = [ { 'billing_frequency': 'monthly', + + expected = [ + { + 'billing_frequency': 'monthly', 'billing_frequency_per': 'month', 'billing_frequency_quantity': 1, 'billing_frequency_unit': 'months', 'code': 'FREE_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, tzinfo=tzutc()), + 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, + tzinfo=tzutc()), 'description': 'A free monthly plan', 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', 'initial_bill_count': 1, @@ -160,12 +165,14 @@ def test_plans_parser(self): 'setup_charge_amount': Decimal('0.00'), 'setup_charge_code': '', 'trial_days': 0}, - { 'billing_frequency': 'monthly', + { + 'billing_frequency': 'monthly', 'billing_frequency_per': 'month', 'billing_frequency_quantity': 1, 'billing_frequency_unit': 'months', 'code': 'PAID_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, tzinfo=tzutc()), + 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, + tzinfo=tzutc()), 'description': '', 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', 'initial_bill_count': 1, @@ -184,445 +191,558 @@ def test_plans_parser(self): pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) self.assertEquals(expected, result) - + def test_plans_parser_with_items(self): plans_xml = self.load_file('plans_with_items.xml') parser = PlansParser() - expected = [ { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'FREE_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, tzinfo=tzutc()), - 'description': 'A free monthly plan', - 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': True, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Monthly Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Free Monthly', - 'recurring_charge_amount': Decimal('0.00'), - 'recurring_charge_code': 'FREE_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}, - { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'TRACKED_MONTHLY', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'description': '', - 'id': 'd19974a6-6e5a-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': False, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': True, - 'name': 'Monthly Item', - 'overage_amount': Decimal('10.00'), - 'quantity_included': Decimal('2')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('10.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Tracked Monthly', - 'recurring_charge_amount': Decimal('10.00'), - 'recurring_charge_code': 'TRACKED_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}, - { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'PAID_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, tzinfo=tzutc()), - 'description': '', - 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': False, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Monthly Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Paid Monthly', - 'recurring_charge_amount': Decimal('20.00'), - 'recurring_charge_code': 'PAID_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}] + expected = [ + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'FREE_MONTHLY', + 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, + tzinfo=tzutc()), + 'description': 'A free monthly plan', + 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': True, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Monthly Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Free Monthly', + 'recurring_charge_amount': Decimal('0.00'), + 'recurring_charge_code': 'FREE_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}, + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'TRACKED_MONTHLY', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'description': '', + 'id': 'd19974a6-6e5a-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': False, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': True, + 'name': 'Monthly Item', + 'overage_amount': Decimal('10.00'), + 'quantity_included': Decimal('2')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('10.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Tracked Monthly', + 'recurring_charge_amount': Decimal('10.00'), + 'recurring_charge_code': 'TRACKED_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}, + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'PAID_MONTHLY', + 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, + tzinfo=tzutc()), + 'description': '', + 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': False, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Monthly Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Paid Monthly', + 'recurring_charge_amount': Decimal('20.00'), + 'recurring_charge_code': 'PAID_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}] result = parser.parse_xml(plans_xml) import pprint pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) self.assertEquals(expected, result) - + def test_customers_parser_with_no_items(self): customers_xml = self.load_file('customers-without-items.xml') parser = CustomersParser() - - expected = [ { 'campaign_content': '', - 'campaign_medium': '', - 'campaign_name': '', - 'campaign_source': '', - 'campaign_term': '', - 'code': 'test', - 'company': '', - 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'email': 'garbage@saaspire.com', - 'first_contact_datetime': None, - 'first_name': 'Test', - 'gateway_token': None, - 'id': '10681b62-6dcd-102e-b098-40402145ee8b', - 'is_vat_exempt': '0', - 'last_name': 'User', - 'meta_data': [ { 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'id': '106953e2-6dcd-102e-b098-40402145ee8b', - 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'name': 'key2', - 'value': 'value_2'}, - { 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'id': '1068b7a2-6dcd-102e-b098-40402145ee8b', - 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'name': 'key_1', - 'value': 'value_1'}], - 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'notes': '', - 'referer': '', - 'referer_host': '', - 'subscriptions': [ { 'cancel_reason': None, - 'cancel_type': None, - 'canceled_datetime': None, - 'cc_address': '', - 'cc_city': '', - 'cc_company': '', - 'cc_country': '', - 'cc_email': None, - 'cc_expiration_date': '', - 'cc_first_name': '', - 'cc_last_four': '', - 'cc_last_name': '', - 'cc_state': '', - 'cc_type': '', - 'cc_zip': '', - 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'gateway_token': '', - 'id': '106953e3-6dcd-102e-b098-40402145ee8b', - 'invoices': [ { 'billing_datetime': datetime(2011, 2, 10, 5, 45, 51, tzinfo=tzutc()), - 'charges': [ { 'code': 'FREE_MONTHLY_RECURRING', - 'created_datetime': datetime(2011, 2, 10, 5, 45, 51, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('0.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'recurring'}], - 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'id': '106ed222-6dcd-102e-b098-40402145ee8b', - 'number': '1', - 'paid_transaction_id': '', - 'type': 'subscription', - 'vat_rate': ''}], - 'items': [], - 'plans': [ { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'FREE_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, tzinfo=tzutc()), - 'description': 'A free monthly plan', - 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': True, - 'items': [], - 'name': 'Free Monthly', - 'recurring_charge_amount': Decimal('0.00'), - 'recurring_charge_code': 'FREE_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}], - 'redirect_url': None}], - 'vat_number': ''}] + + expected = [ + { + 'campaign_content': '', + 'campaign_medium': '', + 'campaign_name': '', + 'campaign_source': '', + 'campaign_term': '', + 'code': 'test', + 'company': '', + 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'email': 'garbage@saaspire.com', + 'first_contact_datetime': None, + 'first_name': 'Test', + 'gateway_token': None, + 'id': '10681b62-6dcd-102e-b098-40402145ee8b', + 'is_vat_exempt': '0', + 'last_name': 'User', + 'meta_data': [ + { + 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'id': '106953e2-6dcd-102e-b098-40402145ee8b', + 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'name': 'key2', + 'value': 'value_2'}, + { + 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'id': '1068b7a2-6dcd-102e-b098-40402145ee8b', + 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'name': 'key_1', + 'value': 'value_1'}], + 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'notes': '', + 'referer': '', + 'referer_host': '', + 'subscriptions': [ + { + 'cancel_reason': None, + 'cancel_type': None, + 'canceled_datetime': None, + 'cc_address': '', + 'cc_city': '', + 'cc_company': '', + 'cc_country': '', + 'cc_email': None, + 'cc_expiration_date': '', + 'cc_first_name': '', + 'cc_last_four': '', + 'cc_last_name': '', + 'cc_state': '', + 'cc_type': '', + 'cc_zip': '', + 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'gateway_token': '', + 'id': '106953e3-6dcd-102e-b098-40402145ee8b', + 'invoices': [ + { + 'billing_datetime': datetime( + 2011, 2, 10, 5, 45, 51, tzinfo=tzutc()), + 'charges': [ + { + 'code': 'FREE_MONTHLY_RECURRING', + 'created_datetime': datetime( + 2011, 2, 10, 5, 45, 51, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('0.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'recurring'}], + 'created_datetime': datetime( + 2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), + 'id': '106ed222-6dcd-102e-b098-40402145ee8b', + 'number': '1', + 'paid_transaction_id': '', + 'type': 'subscription', + 'vat_rate': ''}], + 'items': [], + 'plans': [ + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'FREE_MONTHLY', + 'created_datetime': datetime( + 2011, 1, 7, 20, 46, 43, + tzinfo=tzutc()), + 'description': 'A free monthly plan', + 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': True, + 'items': [], + 'name': 'Free Monthly', + 'recurring_charge_amount': Decimal('0.00'), + 'recurring_charge_code': 'FREE_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}], + 'redirect_url': None}], + 'vat_number': ''}] result = parser.parse_xml(customers_xml) - + import pprint pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) - + self.assertEquals(expected, result) - + def test_customers_parser_with_items(self): customers_xml = self.load_file('customers-with-items.xml') parser = CustomersParser() - - expected = [ { 'campaign_content': '', - 'campaign_medium': '', - 'campaign_name': '', - 'campaign_source': '', - 'campaign_term': '', - 'code': 'test', - 'company': '', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'email': 'garbage@saaspire.com', - 'first_contact_datetime': None, - 'first_name': 'Test', - 'gateway_token': None, - 'id': 'a1f143e0-6e65-102e-b098-40402145ee8b', - 'is_vat_exempt': '0', - 'last_name': 'User', - 'meta_data': [], - 'modified_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'notes': '', - 'referer': '', - 'referer_host': '', - 'subscriptions': [ { 'cancel_reason': None, - 'cancel_type': None, - 'canceled_datetime': None, - 'cc_address': '123 Something St', - 'cc_city': 'Someplace', - 'cc_company': 'Some Co LLC', - 'cc_country': 'United States', - 'cc_email': None, - 'cc_expiration_date': '2011-07-31T00:00:00+00:00', - 'cc_first_name': 'Test', - 'cc_last_four': '1111', - 'cc_last_name': 'User', - 'cc_state': 'NY', - 'cc_type': 'visa', - 'cc_zip': '12345', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'gateway_token': 'SIMULATED', - 'id': 'a1f27c60-6e65-102e-b098-40402145ee8b', - 'invoices': [ { 'billing_datetime': datetime(2011, 2, 10, 23, 57, 58, tzinfo=tzutc()), - 'charges': [ { 'code': 'TRACKED_MONTHLY_RECURRING', - 'created_datetime': datetime(2011, 2, 10, 23, 57, 58, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('10.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'recurring'}, - { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('10.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'item'}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('10.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'item'}], - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'id': 'a1f7faaa-6e65-102e-b098-40402145ee8b', - 'number': '1', - 'paid_transaction_id': '', - 'type': 'subscription', - 'vat_rate': ''}], - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'modified_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'name': 'Monthly Item', - 'quantity': Decimal('3')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'modified_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'name': 'Once Item', - 'quantity': Decimal('1')}], - 'plans': [ { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'TRACKED_MONTHLY', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'description': '', - 'id': 'd19974a6-6e5a-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': False, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': True, - 'name': 'Monthly Item', - 'overage_amount': Decimal('10.00'), - 'quantity_included': Decimal('2')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('10.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Tracked Monthly', - 'recurring_charge_amount': Decimal('10.00'), - 'recurring_charge_code': 'TRACKED_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}], - 'redirect_url': None}], - 'vat_number': ''}] + + expected = [ + { + 'campaign_content': '', + 'campaign_medium': '', + 'campaign_name': '', + 'campaign_source': '', + 'campaign_term': '', + 'code': 'test', + 'company': '', + 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'email': 'garbage@saaspire.com', + 'first_contact_datetime': None, + 'first_name': 'Test', + 'gateway_token': None, + 'id': 'a1f143e0-6e65-102e-b098-40402145ee8b', + 'is_vat_exempt': '0', + 'last_name': 'User', + 'meta_data': [], + 'modified_datetime': datetime(2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'notes': '', + 'referer': '', + 'referer_host': '', + 'subscriptions': [ + { + 'cancel_reason': None, + 'cancel_type': None, + 'canceled_datetime': None, + 'cc_address': '123 Something St', + 'cc_city': 'Someplace', + 'cc_company': 'Some Co LLC', + 'cc_country': 'United States', + 'cc_email': None, + 'cc_expiration_date': '2011-07-31T00:00:00+00:00', + 'cc_first_name': 'Test', + 'cc_last_four': '1111', + 'cc_last_name': 'User', + 'cc_state': 'NY', + 'cc_type': 'visa', + 'cc_zip': '12345', + 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'gateway_token': 'SIMULATED', + 'id': 'a1f27c60-6e65-102e-b098-40402145ee8b', + 'invoices': [ + { + 'billing_datetime': datetime(2011, 2, 10, 23, + 57, 58, + tzinfo=tzutc()), + 'charges': [ + { + 'code': 'TRACKED_MONTHLY_RECURRING', + 'created_datetime': datetime( + 2011, 2, 10, 23, 57, 58, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('10.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'recurring'}, + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('10.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'item'}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('10.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'item'}], + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'id': 'a1f7faaa-6e65-102e-b098-40402145ee8b', + 'number': '1', + 'paid_transaction_id': '', + 'type': 'subscription', + 'vat_rate': ''}], + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'modified_datetime': datetime( + 2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), + 'name': 'Monthly Item', + 'quantity': Decimal('3')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'modified_datetime': datetime( + 2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), + 'name': 'Once Item', + 'quantity': Decimal('1')}], + 'plans': [ + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'TRACKED_MONTHLY', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), + 'description': '', + 'id': 'd19974a6-6e5a-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': False, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': True, + 'name': 'Monthly Item', + 'overage_amount': Decimal('10.00'), + 'quantity_included': Decimal('2')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('10.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Tracked Monthly', + 'recurring_charge_amount': Decimal('10.00'), + 'recurring_charge_code': 'TRACKED_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}], + 'redirect_url': None}], + 'vat_number': ''}] result = parser.parse_xml(customers_xml) - + import pprint pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) self.assertEquals(expected, result) - #import pprint - #pp = pprint.PrettyPrinter(indent=4) - #pp.pprint(result) - #assert False def test_paypal_customer_parse(self): customers_xml = self.load_file('paypal_customer.xml') parser = CustomersParser() - - expected = [ { 'campaign_content': '', - 'campaign_medium': '', - 'campaign_name': '', - 'campaign_source': '', - 'campaign_term': '', - 'code': 'test', - 'company': '', - 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'email': 'garbage@saaspire.com', - 'first_contact_datetime': None, - 'first_name': 'Test', - 'gateway_token': None, - 'id': '95d7696a-7fda-11e0-a51b-40403c39f8d9', - 'is_vat_exempt': '0', - 'last_name': 'User', - 'meta_data': [], - 'modified_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'notes': '', - 'referer': '', - 'referer_host': '', - 'subscriptions': [ { 'cancel_reason': 'PayPal preapproval is pending', - 'cancel_type': 'paypal-wait', - 'canceled_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'cc_address': '', - 'cc_city': '', - 'cc_company': '', - 'cc_country': '', - 'cc_email': '', - 'cc_expiration_date': '2012-05-16T00:00:00+00:00', - 'cc_first_name': 'Test', - 'cc_last_four': '', - 'cc_last_name': 'User', - 'cc_state': '', - 'cc_type': '', - 'cc_zip': '', - 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'gateway_account': { 'gateway': 'PayPal_Simulator', - 'id': '303f9a50-7fda-11e0-a51b-40403c39f8d9', - 'type': 'paypal'}, - 'gateway_token': 'SIMULATED-4dd152718371a', - 'id': '95d804ba-7fda-11e0-a51b-40403c39f8d9', - 'invoices': [ { 'billing_datetime': datetime(2011, 6, 16, 16, 36, 1, tzinfo=tzutc()), - 'charges': [ { 'code': 'PAID_MONTHLY_RECURRING', - 'created_datetime': datetime(2011, 6, 16, 16, 36, 1, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('20.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'recurring'}], - 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'id': '95de499c-7fda-11e0-a51b-40403c39f8d9', - 'number': '1', - 'paid_transaction_id': '', - 'type': 'subscription', - 'vat_rate': ''}], - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': None, - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'modified_datetime': None, - 'name': 'Monthly Item', - 'quantity': Decimal('0')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': None, - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'modified_datetime': None, - 'name': 'Once Item', - 'quantity': Decimal('0')}], - 'plans': [ { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'PAID_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, tzinfo=tzutc()), - 'description': '', - 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': False, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Monthly Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Paid Monthly', - 'recurring_charge_amount': Decimal('20.00'), - 'recurring_charge_code': 'PAID_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}], - 'redirect_url': 'https://cheddargetter.com/service/paypal/simulate/productId/2ccbecd6-6beb-102e-b098-40402145ee8b/id/95d7696a-7fda-11e0-a51b-40403c39f8d9?preapprovalkey=SIMULATED-4dd152718371a'}], - 'vat_number': ''}] + + expected = [ + { + 'campaign_content': '', + 'campaign_medium': '', + 'campaign_name': '', + 'campaign_source': '', + 'campaign_term': '', + 'code': 'test', + 'company': '', + 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, + tzinfo=tzutc()), + 'email': 'garbage@saaspire.com', + 'first_contact_datetime': None, + 'first_name': 'Test', + 'gateway_token': None, + 'id': '95d7696a-7fda-11e0-a51b-40403c39f8d9', + 'is_vat_exempt': '0', + 'last_name': 'User', + 'meta_data': [], + 'modified_datetime': datetime(2011, 5, 16, 16, 36, 1, + tzinfo=tzutc()), + 'notes': '', + 'referer': '', + 'referer_host': '', + 'subscriptions': [ + { + 'cancel_reason': 'PayPal preapproval is pending', + 'cancel_type': 'paypal-wait', + 'canceled_datetime': datetime(2011, 5, 16, 16, 36, 1, + tzinfo=tzutc()), + 'cc_address': '', + 'cc_city': '', + 'cc_company': '', + 'cc_country': '', + 'cc_email': '', + 'cc_expiration_date': '2012-05-16T00:00:00+00:00', + 'cc_first_name': 'Test', + 'cc_last_four': '', + 'cc_last_name': 'User', + 'cc_state': '', + 'cc_type': '', + 'cc_zip': '', + 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, + tzinfo=tzutc()), + 'gateway_account': { + 'gateway': 'PayPal_Simulator', + 'id': '303f9a50-7fda-11e0-a51b-40403c39f8d9', + 'type': 'paypal'}, + 'gateway_token': 'SIMULATED-4dd152718371a', + 'id': '95d804ba-7fda-11e0-a51b-40403c39f8d9', + 'invoices': [ + { + 'billing_datetime': datetime( + 2011, 6, 16, 16, 36, 1, tzinfo=tzutc()), + 'charges': [ + { + 'code': 'PAID_MONTHLY_RECURRING', + 'created_datetime': datetime( + 2011, 6, 16, 16, 36, 1, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('20.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'recurring'}], + 'created_datetime': datetime( + 2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), + 'id': '95de499c-7fda-11e0-a51b-40403c39f8d9', + 'number': '1', + 'paid_transaction_id': '', + 'type': 'subscription', + 'vat_rate': ''}], + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': None, + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'modified_datetime': None, + 'name': 'Monthly Item', + 'quantity': Decimal('0')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': None, + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'modified_datetime': None, + 'name': 'Once Item', + 'quantity': Decimal('0')}], + 'plans': [ + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'PAID_MONTHLY', + 'created_datetime': datetime( + 2011, 1, 7, 21, 5, 42, tzinfo=tzutc()), + 'description': '', + 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': False, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Monthly Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Paid Monthly', + 'recurring_charge_amount': Decimal('20.00'), + 'recurring_charge_code': 'PAID_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}], + 'redirect_url': 'https://cheddargetter.com/service/paypal/simulate/productId/2ccbecd6-6beb-102e-b098-40402145ee8b/id/95d7696a-7fda-11e0-a51b-40403c39f8d9?preapprovalkey=SIMULATED-4dd152718371a'}], + 'vat_number': ''}] result = parser.parse_xml(customers_xml) - + import pprint pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) self.assertEquals(expected, result) - diff --git a/tests/product_tests.py b/tests/product_tests.py index b834c8d..ae066d0 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -2,13 +2,12 @@ import string from copy import copy -from datetime import date, datetime, timedelta +from datetime import datetime, timedelta from decimal import Decimal import unittest -from unittest.case import SkipTest from dateutil.relativedelta import relativedelta -from dateutil.tz import * +from dateutil.tz import tzutc from nose.tools import raises from testconfig import config @@ -17,26 +16,27 @@ from testing_tools.decorators import clear_users + class ProductTests(unittest.TestCase): - - client_defaults = { + + client_defaults = { 'username': config['cheddar']['username'], 'password': config['cheddar']['password'], 'product_code': config['cheddar']['product_code'], 'endpoint': config['cheddar']['endpoint'], } - + customer_defaults = { 'code': 'test', - 'email':'garbage@saaspire.com', + 'email': 'garbage@saaspire.com', 'first_name': 'Test', 'last_name': 'User', 'plan_code': 'FREE_MONTHLY', } - paypal_defaults = { + paypal_defaults = { 'code': 'test', - 'email':'garbage@saaspire.com', + 'email': 'garbage@saaspire.com', 'first_name': 'Test', 'last_name': 'User', 'plan_code': 'PAID_MONTHLY', @@ -47,9 +47,9 @@ class ProductTests(unittest.TestCase): 'return_url': 'http://example.com/return', 'cancel_url': 'http://example.com/cancel', } - + exipration = datetime.now() + timedelta(days=180) - + paid_defaults = { 'cc_number': '4111111111111111', 'cc_expiration': exipration.strftime('%m/%Y'), @@ -64,10 +64,10 @@ class ProductTests(unittest.TestCase): 'cc_zip': '12345', 'plan_code': 'PAID_MONTHLY', } - + def get_product(self): product = CheddarProduct(**self.client_defaults) - + return product def test_repr(self): @@ -79,15 +79,15 @@ def test_repr(self): def test_instantiate_product(self): product = self.get_product() - + for key, value in self.client_defaults.items(): self.assertEquals(value, getattr(product.client, key)) - + def test_get_all_plans(self): product = self.get_product() - + plans = product.get_all_plans() - + for plan in plans: if plan.code == 'FREE_MONTHLY': free_plan = plan @@ -95,28 +95,28 @@ def test_get_all_plans(self): paid_plan = plan elif plan.code == 'TRACKED_MONTHLY': tracked_plan = plan - + self.assertEquals('FREE_MONTHLY', free_plan.code) self.assertEquals('PAID_MONTHLY', paid_plan.code) self.assertEquals('TRACKED_MONTHLY', tracked_plan.code) - + def test_get_plan(self): product = self.get_product() code = 'PAID_MONTHLY' plan = product.get_plan(code) - + self.assertEquals(code, plan.code) - + def test_plan_initial_bill_date(self): product = self.get_product() code = 'PAID_MONTHLY' plan = product.get_plan(code) - + expected = datetime.utcnow().date() + relativedelta(months=1) result = plan.initial_bill_date - + self.assertEquals(expected, result) - + def get_customer(self, **kwargs): customer_data = copy(self.customer_defaults) # We need to make unique customers with the same data. @@ -128,11 +128,11 @@ def get_customer(self, **kwargs): customer_data.update({'notes': random_string}) customer_data.update(kwargs) product = self.get_product() - + customer = product.create_customer(**customer_data) - + return customer - + def get_customer_with_items(self, **kwargs): data = copy(self.paid_defaults) if 'items' in kwargs.keys(): @@ -141,11 +141,11 @@ def get_customer_with_items(self, **kwargs): items = [] items.append({'code': 'MONTHLY_ITEM', 'quantity': 3}) items.append({'code': 'ONCE_ITEM'}) - + data['items'] = items data['plan_code'] = 'TRACKED_MONTHLY' customer = self.get_customer(**data) - + return customer @clear_users @@ -158,7 +158,7 @@ def test_create_customer_with_company(self): @clear_users def test_create_customer_with_meta_data(self): - self.get_customer(meta_data = {'key_1': 'value_1', 'key2': 'value_2'}) + self.get_customer(meta_data={'key_1': 'value_1', 'key2': 'value_2'}) @clear_users def test_create_customer_with_true_vat_exempt(self): @@ -206,8 +206,8 @@ def test_create_customer_with_initial_bill_date(self): customer = self.get_customer(initial_bill_date=initial_bill_date) invoice = customer.subscription.invoices[0] real_bill_date = invoice['billing_datetime'] - - # Sometimes cheddar getter will push the bill date to the next day + + # Sometimes cheddar getter will push the bill date to the next day # if the request is made around UTC midnight diff = initial_bill_date.date() - real_bill_date.date() self.assertLessEqual(diff.days, 1) @@ -220,7 +220,7 @@ def test_create_paid_customer(self): def test_create_paid_customer_with_charges(self): data = copy(self.paid_defaults) charges = [] - charges.append({'code': 'test_charge_1', 'each_amount':2}) + charges.append({'code': 'test_charge_1', 'each_amount': 2}) charges.append({'code': 'charge2', 'quantity': 3, 'each_amount': 4}) data['charges'] = charges self.get_customer(**data) @@ -229,7 +229,8 @@ def test_create_paid_customer_with_charges(self): def test_create_paid_customer_with_decimal_charges(self): data = copy(self.paid_defaults) charges = [] - charges.append({'code': 'test_charge_1', 'each_amount': Decimal('2.30')}) + charges.append({'code': 'test_charge_1', + 'each_amount': Decimal('2.30')}) charges.append({'code': 'charge2', 'each_amount': Decimal('-4.5')}) data['charges'] = charges self.get_customer(**data) @@ -282,10 +283,10 @@ def test_customer_repr(self): def test_subscription_repr(self): customer = self.get_customer() subscription = customer.subscription - + expected = 'Subscription:' result = repr(subscription) - + self.assertIn(expected, result) @clear_users @@ -293,10 +294,10 @@ def test_pricing_plan_repr(self): customer = self.get_customer() subscription = customer.subscription plan = subscription.plan - + expected = 'PricingPlan: Free Monthly (FREE_MONTHLY)' result = repr(plan) - + self.assertEquals(expected, result) @clear_users @@ -304,25 +305,26 @@ def test_item_repr(self): customer = self.get_customer_with_items() subscription = customer.subscription item = subscription.items['MONTHLY_ITEM'] - + expected = 'Item: MONTHLY_ITEM for test' result = repr(item) - + self.assertEquals(expected, result) @clear_users def test_get_customers(self): - customer1 = self.get_customer() + ''' Create two customers, verify 2 returned. ''' + self.get_customer() customer2_data = copy(self.paid_defaults) customer2_data.update({ 'code': 'test2', - 'email':'garbage+2@saaspire.com', + 'email': 'garbage+2@saaspire.com', 'first_name': 'Test2', 'last_name': 'User2', }) - customer2 = self.get_customer(**customer2_data) + self.get_customer(**customer2_data) product = self.get_product() - + fetched_customers = product.get_customers() self.assertEquals(2, len(fetched_customers)) @@ -330,12 +332,14 @@ def test_get_customers(self): def test_get_customer(self): created_customer = self.get_customer() product = self.get_product() - + fetched_customer = product.get_customer(code=created_customer.code) - + self.assertEquals(created_customer.code, fetched_customer.code) - self.assertEquals(created_customer.first_name, fetched_customer.first_name) - self.assertEquals(created_customer.last_name, fetched_customer.last_name) + self.assertEquals(created_customer.first_name, + fetched_customer.first_name) + self.assertEquals(created_customer.last_name, + fetched_customer.last_name) self.assertEquals(created_customer.email, fetched_customer.email) @clear_users @@ -343,10 +347,10 @@ def test_simple_customer_update(self): new_name = 'Different' customer = self.get_customer() product = self.get_product() - + customer.update(first_name=new_name) self.assertEquals(new_name, customer.first_name) - + fetched_customer = product.get_customer(code=customer.code) self.assertEquals(customer.first_name, fetched_customer.first_name) @@ -355,25 +359,28 @@ def test_simple_customer_update(self): def test_delete_customer(self): customer = self.get_customer() product = self.get_product() - + fetched_customer = product.get_customer(code=customer.code) self.assertEquals(customer.first_name, fetched_customer.first_name) - + customer.delete() fetched_customer = product.get_customer(code=customer.code) - @clear_users def test_delete_all_customers(self): - customer_1 = self.get_customer() - customer_2 = self.get_customer(code='test2') + ''' + Create two customers, verify 2 returned, + delete and verify 0 customers. + ''' + self.get_customer() + self.get_customer(code='test2') product = self.get_product() - + fetched_customers = product.get_customers() self.assertEquals(2, len(fetched_customers)) - + product.delete_all_customers() - + fetched_customers = product.get_customers() self.assertEquals(0, len(fetched_customers)) @@ -381,28 +388,28 @@ def test_delete_all_customers(self): def test_cancel_subscription(self): customer = self.get_customer() customer.subscription.cancel() - + now = datetime.now(tzutc()) canceled_on = customer.subscription.canceled diff = now - canceled_on limit = timedelta(seconds=10) self.assertLess(diff, limit) - + def assert_increment(self, quantity=None): customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] - + old_quantity = item.quantity_used item.increment(quantity) new_quantity = item.quantity_used diff = new_quantity - old_quantity expected = Decimal(quantity or 1) self.assertAlmostEqual(expected, diff, places=2) - + fetched_customer = product.get_customer(code=customer.code) - fetched_item = customer.subscription.items[item.code] + fetched_item = fetched_customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) @clear_users @@ -420,21 +427,21 @@ def test_float_increment(self): @clear_users def test_decimal_increment(self): self.assert_increment(Decimal('1.234')) - + def assert_decrement(self, quantity=None): customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] - + old_quantity = item.quantity_used item.decrement(quantity) new_quantity = item.quantity_used diff = old_quantity - new_quantity expected = Decimal(quantity or 1) self.assertAlmostEqual(expected, diff, places=2) - + fetched_customer = product.get_customer(code=customer.code) - fetched_item = customer.subscription.items[item.code] + fetched_item = fetched_customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) @clear_users @@ -452,20 +459,19 @@ def test_float_decrement(self): @clear_users def test_decimal_decrement(self): self.assert_decrement(Decimal('1.234')) - + def assert_set(self, quantity): customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] - - old_quantity = item.quantity_used + item.set(quantity) new_quantity = item.quantity_used expected = Decimal(quantity) self.assertAlmostEqual(expected, new_quantity, places=2) - + fetched_customer = product.get_customer(code=customer.code) - fetched_item = customer.subscription.items[item.code] + fetched_item = fetched_customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) @clear_users @@ -479,40 +485,42 @@ def test_float_set(self): @clear_users def test_decimal_set(self): self.assert_set(Decimal('1.234')) - + def assert_charged(self, code, each_amount, quantity=None, - description=None): + description=None): customer = self.get_customer(**self.paid_defaults) product = self.get_product() - + customer.charge( code=code, each_amount=each_amount, quantity=quantity, description=description, ) - + if description is None: description = '' - + found_charge = None for invoice in customer.subscription.invoices: for charge in invoice['charges']: if charge['code'] == code: found_charge = charge - - self.assertAlmostEqual(Decimal(each_amount), found_charge['each_amount'], places=2) + + self.assertAlmostEqual(Decimal(each_amount), + found_charge['each_amount'], places=2) self.assertEqual(quantity, found_charge['quantity']) self.assertEqual(description, found_charge['description']) - + fetched_customer = product.get_customer(code=customer.code) fetched_charge = None for invoice in fetched_customer.subscription.invoices: for charge in invoice['charges']: if charge['code'] == code: fetched_charge = charge - - self.assertAlmostEqual(Decimal(each_amount), fetched_charge['each_amount'], places=2) + + self.assertAlmostEqual(Decimal(each_amount), + fetched_charge['each_amount'], places=2) self.assertEqual(quantity, fetched_charge['quantity']) self.assertEqual(description, fetched_charge['description']) @@ -526,29 +534,32 @@ def test_add_float_charge(self): @clear_users def test_add_decimal_charge(self): - self.assert_charged(code='TEST-CHARGE', each_amount=Decimal('2.3'), quantity=3) + self.assert_charged(code='TEST-CHARGE', each_amount=Decimal('2.3'), + quantity=3) @clear_users def test_add_charge_with_descriptions(self): - self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1, description="A test charge") + self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1, + description="A test charge") @clear_users def test_add_credit(self): self.assert_charged(code='TEST-CHARGE', each_amount=-1, quantity=1) - def assertCharge(self, customer, code, each_amount, quantity, description='', invoice_type=None): + def assertCharge(self, customer, code, each_amount, quantity, + description='', invoice_type=None): found_charge = None for invoice in customer.subscription.invoices: if invoice_type is None or invoice['type'] == invoice_type: for charge in invoice['charges']: if charge['code'] == code: found_charge = charge - - self.assertAlmostEqual(Decimal(each_amount), found_charge['each_amount'], places=2) + + self.assertAlmostEqual(Decimal(each_amount), + found_charge['each_amount'], places=2) self.assertEqual(quantity, found_charge['quantity']) self.assertEqual(description, found_charge['description']) - def assertOneTimeInvoice(self, charges): customer = self.get_customer(**self.paid_defaults) product = self.get_product() @@ -558,59 +569,62 @@ def assertOneTimeInvoice(self, charges): for charge in charges: self.assertCharge( customer, - code = charge['code'], - quantity = charge['quantity'], - each_amount = charge['each_amount'], - description = charge.get('description', ''), - invoice_type = 'one-time', + code=charge['code'], + quantity=charge['quantity'], + each_amount=charge['each_amount'], + description=charge.get('description', ''), + invoice_type='one-time', ) fetched_customer = product.get_customer(code=customer.code) for charge in charges: self.assertCharge( fetched_customer, - code = charge['code'], - quantity = charge['quantity'], - each_amount = charge['each_amount'], - description = charge.get('description', ''), - invoice_type = 'one-time', + code=charge['code'], + quantity=charge['quantity'], + each_amount=charge['each_amount'], + description=charge.get('description', ''), + invoice_type='one-time', ) @clear_users def test_add_simple_one_time_invoice(self): - charges = [{ - 'code': 'immediate-test', - 'quantity': 1, - 'each_amount': Decimal(5.23) - },] + charges = [ + { + 'code': 'immediate-test', + 'quantity': 1, + 'each_amount': Decimal(5.23) + }] self.assertOneTimeInvoice(charges) @clear_users def test_add_one_time_invoice_with_description(self): - charges = [{ - 'code': 'immediate-test', - 'quantity': 1, - 'each_amount': Decimal(5.23), - 'description': 'This is a test charge' - },] + charges = [ + { + 'code': 'immediate-test', + 'quantity': 1, + 'each_amount': Decimal(5.23), + 'description': 'This is a test charge' + }] self.assertOneTimeInvoice(charges) @clear_users def test_add_one_time_invoice_with_multiple_charges(self): - charges = [{ - 'code': 'immediate-test', - 'quantity': 1, - 'each_amount': Decimal(5.23), - 'description': 'This is a test charge' - }, - { - 'code': 'immediate-test-2', - 'quantity': 3, - 'each_amount': 15, - 'description': 'This is another test charge' - },] + charges = [ + { + 'code': 'immediate-test', + 'quantity': 1, + 'each_amount': Decimal(5.23), + 'description': 'This is a test charge' + }, + { + 'code': 'immediate-test-2', + 'quantity': 3, + 'each_amount': 15, + 'description': 'This is another test charge' + }] self.assertOneTimeInvoice(charges) @@ -632,7 +646,8 @@ def test_get_promotion(self): self.assertEqual(promotion.name, 'Coupon') self.assertEqual(promotion.coupons[0].get('code'), 'COUPON') self.assertEqual(promotion.incentives[0].get('percentage'), '10') - self.assertEqual(promotion.incentives[0].get('expiration_datetime'), None) + self.assertEqual(promotion.incentives[0].get('expiration_datetime'), + None) def test_promotion_repr(self): ''' Test the internal __repr___ method of Promotion. ''' @@ -641,7 +656,7 @@ def test_promotion_repr(self): expected = 'Promotion: Coupon (COUPON)' result = repr(promotion) - + self.assertEquals(expected, result) def test_promotion_unicode(self): @@ -651,5 +666,5 @@ def test_promotion_unicode(self): expected = 'Coupon (COUPON)' result = unicode(promotion) - + self.assertEquals(expected, result) diff --git a/tests/testing_tools/decorators.py b/tests/testing_tools/decorators.py index c5fe985..39dc774 100644 --- a/tests/testing_tools/decorators.py +++ b/tests/testing_tools/decorators.py @@ -2,20 +2,20 @@ from utils import clear_users as clear_users_func + def clear_users(func): ''' Calls cheddar's delete all users method no matter the test result ''' def new(*args, **kwargs): - raised_exception = None try: func(*args, **kwargs) - except Exception, e: + except Exception: clear_users_func() raise - + clear_users_func() - + new = make_decorator(func)(new) - - return new \ No newline at end of file + + return new diff --git a/tests/testing_tools/utils.py b/tests/testing_tools/utils.py index 0b5d616..68cb0c8 100644 --- a/tests/testing_tools/utils.py +++ b/tests/testing_tools/utils.py @@ -3,6 +3,7 @@ from testconfig import config + def clear_users(): username = config['cheddar']['username'] password = config['cheddar']['password'] @@ -12,9 +13,13 @@ def clear_users(): h = httplib2.Http() h.add_credentials(username, password) - url = '%s/customers/delete-all/confirm/%d/productCode/%s' % (endpoint, int(time()), product) + url = '%s/customers/delete-all/confirm/%d/productCode/%s' % (endpoint, + int(time()), + product) response, content = h.request(url, 'POST') if response.status != 200 or 'success' not in content: - raise Exception('Could not clear users. Recieved a response of %s %s \n %s' % (response.status, response.reason, content)) + raise Exception( + 'Could not clear users. Recieved a response of %s %s \n %s' % ( + response.status, response.reason, content)) From a8032478f0ae4334738e96ede4af421bf4c92c80 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 17:45:27 -0400 Subject: [PATCH 26/53] Tried to add a return and failed due to the PEP8 on the unused response. We just won't store it. --- sharpy/product.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sharpy/product.py b/sharpy/product.py index a2e2414..80bb296 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -275,11 +275,10 @@ def delete_all_customers(self): DO NOT RUN THIS UNLESS YOU REALLY, REALLY, REALLY MEAN TO! ''' - response = self.client.make_request( + self.client.make_request( path='customers/delete-all/confirm/%d' % int(time()), method='POST' ) - return self.load_data_from_xml(response.content) def get_all_promotions(self): ''' @@ -517,11 +516,10 @@ def update(self, first_name=None, last_name=None, email=None, def delete(self): path = 'customers/delete' params = {'code': self.code} - response = self.product.client.make_request( + self.product.client.make_request( path=path, params=params, ) - return self.load_data_from_xml(response.content) def charge(self, code, each_amount, quantity=1, description=None): ''' From b18e79446b4cbe38140bcde382199a043f1b9390 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Thu, 27 Aug 2015 17:46:12 -0400 Subject: [PATCH 27/53] Add docstrings to the tests. Also add a sleep for a test that fails sparadically. I think network traffic or slowness on chedder servers is at play. --- tests/client_tests.py | 30 +++++++++++++++-- tests/parser_tests.py | 20 ++++++++++++ tests/product_tests.py | 73 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/tests/client_tests.py b/tests/client_tests.py index 4841b3b..370da62 100644 --- a/tests/client_tests.py +++ b/tests/client_tests.py @@ -28,6 +28,7 @@ class ClientTests(unittest.TestCase): } def get_client(self, **kwargs): + ''' Helper mthod for instantiating client. ''' client_kwargs = copy(self.client_defaults) client_kwargs.update(kwargs) @@ -36,6 +37,7 @@ def get_client(self, **kwargs): return c def try_client(self, **kwargs): + ''' Helper method for getting client.''' args = copy(self.client_defaults) args.update(kwargs) client = self.get_client(**kwargs) @@ -50,12 +52,15 @@ def try_client(self, **kwargs): self.assertEquals(client.default_endpoint, client.endpoint) def test_basic_init(self): + ''' Test basic initialization. ''' self.try_client() def test_custom_endpoint_init(self): + ''' Test initialization with custom endpoint. ''' self.try_client(endpoint='http://cheddar-test.saaspire.com') def try_url_build(self, path, params=None): + ''' Helper method for client build_url method. ''' c = self.get_client() expected = u'%s/%s/productCode/%s' % ( c.endpoint, @@ -71,20 +76,24 @@ def try_url_build(self, path, params=None): self.assertEquals(expected, result) def test_basic_build_url(self): + ''' Test basic client build_url. ''' path = 'users' self.try_url_build(path) def test_single_param_build_url(self): + ''' Test client build_url with single parameter. ''' path = 'users' params = {'key': 'value'} self.try_url_build(path, params) def test_multi_param_build_url(self): + ''' Test client build_url with multiple parameters. ''' path = 'users' params = {'key1': 'value1', 'key2': 'value2'} self.try_url_build(path, params) def test_make_request(self): + ''' Test client make_request method. ''' path = 'plans/get' client = self.get_client() response = client.make_request(path) @@ -93,6 +102,7 @@ def test_make_request(self): @raises(AccessDenied) def test_make_request_access_denied(self): + ''' Test client make_request method with bad username. ''' path = 'plans/get' bad_username = self.client_defaults['username'] + '_bad' client = self.get_client(username=bad_username) @@ -100,19 +110,21 @@ def test_make_request_access_denied(self): @raises(NotFound) def test_make_request_bad_request(self): - """ Attempt to grab the plans without adding /get to the url. """ + ''' Attempt to grab the plans without adding /get to the url. ''' path = 'plans' client = self.get_client() client.make_request(path) @raises(NotFound) def test_make_request_not_found(self): + ''' Test client make_request method with bad path. ''' path = 'things-which-dont-exist' client = self.get_client() client.make_request(path) @clear_users def test_post_request(self): + ''' Test client make_request method as HTTP POST. ''' path = 'customers/new' data = { 'code': 'post_test', @@ -162,6 +174,7 @@ def generate_error_response(self, auxcode=None, path=None, params=None, def assertCheddarError(self, auxcode, expected_exception, path=None, params=None): + ''' Helper method for verifing a Cheddar Error is raised. ''' assert_raises( expected_exception, self.generate_error_response, @@ -171,11 +184,13 @@ def assertCheddarError(self, auxcode, expected_exception, path=None, ) def assertCheddarErrorForAuxCodes(self, auxcodes, expected_exception): + ''' Helper method for verifing a Cheddar Aux Code Error is raised. ''' for auxcode in auxcodes: self.assertCheddarError(auxcode, expected_exception) @clear_users def test_cheddar_500s(self): + ''' Test a 500 HTTP status code on CheddarGetter. ''' auxcodes = (1000, 1002, 1003) expected_exception = CheddarFailure self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) @@ -193,18 +208,21 @@ def test_cheddar_400(self): @clear_users def test_cheddar_401s(self): + ''' Test a 401 HTTP status code on CheddarGetter. ''' auxcodes = (2000, 2001, 2002, 2003) expected_exception = AccessDenied self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) @clear_users def test_cheddar_502s(self): + ''' Test a 502 HTTP status code on CheddarGetter. ''' auxcodes = (3000, 4000) expected_exception = NaughtyGateway self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) @clear_users def test_cheddar_422s(self): + ''' Test a 422 HTTP status code on CheddarGetter. ''' auxcodes = (5000, 5001, 5002, 5003, 6000, 6001, 6002, 7000) expected_exception = UnprocessableEntity self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) @@ -212,9 +230,11 @@ def test_cheddar_422s(self): @clear_users @raises(PreconditionFailed) def test_cheddar_412s(self): + ''' Test a 412 HTTP status code on CheddarGetter. ''' self.generate_error_response(auxcode=2345, firstName='') def test_format_datetime_with_datetime(self): + ''' Test client format_datetime method. ''' client = self.get_client() result = client.format_datetime(datetime( year=2010, month=9, day=19, hour=20, minute=10, second=39)) @@ -223,6 +243,7 @@ def test_format_datetime_with_datetime(self): self.assertEquals(expected, result) def test_format_datetime_with_datetime_with_tz(self): + ''' Test client format_datetime method with timezone info. ''' client = self.get_client() result = client.format_datetime(datetime( year=2010, month=9, day=19, hour=20, minute=10, second=39, @@ -232,6 +253,7 @@ def test_format_datetime_with_datetime_with_tz(self): self.assertEquals(expected, result) def test_format_datetime_with_date(self): + ''' Test client format_datetime method with date. ''' client = self.get_client() result = client.format_datetime(date(year=2010, month=9, day=19)) expected = '2010-09-19T00:00:00+00:00' @@ -239,6 +261,7 @@ def test_format_datetime_with_date(self): self.assertEquals(expected, result) def test_format_date_with_now(self): + ''' Test client format_date method with now. ''' client = self.get_client() result = client.format_date('now') expected = 'now' @@ -246,6 +269,7 @@ def test_format_date_with_now(self): self.assertEquals(expected, result) def test_format_datetime_with_now(self): + ''' Test client format_datetime method with now. ''' client = self.get_client() result = client.format_datetime('now') expected = 'now' @@ -254,9 +278,9 @@ def test_format_datetime_with_now(self): @clear_users def test_chedder_update_customer_error(self): - """ + ''' Test overriding the zipcode so a customer actually gets updated. - """ + ''' overrides = { 'subscription[ccZip]': 12345 } diff --git a/tests/parser_tests.py b/tests/parser_tests.py index ecfd15c..9ab3a37 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -16,6 +16,7 @@ class ParserTests(unittest.TestCase): def load_file(self, filename): + ''' Helper method to load an xml file from the files directory. ''' path = os.path.join(os.path.dirname(__file__), 'files', filename) f = open(path) content = f.read() @@ -23,6 +24,7 @@ def load_file(self, filename): return content def test_bool_parsing_true(self): + ''' Test boolean parsing evaluates to true. ''' parser = CheddarOutputParser() expected = True @@ -31,6 +33,7 @@ def test_bool_parsing_true(self): self.assertEquals(expected, result) def test_bool_parsing_false(self): + ''' Test boolean parsing evaluates to false. ''' parser = CheddarOutputParser() expected = False @@ -40,11 +43,13 @@ def test_bool_parsing_false(self): @raises(ParseError) def test_bool_parsing_error(self): + ''' Test boolean parsing with non-boolean string. ''' parser = CheddarOutputParser() parser.parse_bool('test') def test_bool_parsing_empty(self): + ''' Test boolean parsing with empty string. ''' parser = CheddarOutputParser() expected = None @@ -53,6 +58,7 @@ def test_bool_parsing_empty(self): self.assertEquals(expected, result) def test_int_parsing(self): + ''' Test integer parsing with integer as string. ''' parser = CheddarOutputParser() expected = 234 @@ -62,11 +68,13 @@ def test_int_parsing(self): @raises(ParseError) def test_int_parsing_error(self): + ''' Test integer parsing with non-integer string. ''' parser = CheddarOutputParser() parser.parse_int('test') def test_int_parsing_empty(self): + ''' Test integer parsing with empty string. ''' parser = CheddarOutputParser() expected = None @@ -75,6 +83,7 @@ def test_int_parsing_empty(self): self.assertEquals(expected, result) def test_decimal_parsing(self): + ''' Test decimal parsing with decimal string. ''' parser = CheddarOutputParser() expected = Decimal('2.345') @@ -84,11 +93,13 @@ def test_decimal_parsing(self): @raises(ParseError) def test_decimal_parsing_error(self): + ''' Test decimal parsing with non-decimal string. ''' parser = CheddarOutputParser() parser.parse_decimal('test') def test_decimal_parsing_empty(self): + ''' Test decimal parsing with empty string. ''' parser = CheddarOutputParser() expected = None @@ -97,6 +108,7 @@ def test_decimal_parsing_empty(self): self.assertEquals(expected, result) def test_datetime_parsing(self): + ''' Test datetime parsing. ''' parser = CheddarOutputParser() expected = datetime( @@ -114,11 +126,13 @@ def test_datetime_parsing(self): @raises(ParseError) def test_datetime_parsing_error(self): + ''' Test datetime parsing with non-date string. ''' parser = CheddarOutputParser() parser.parse_datetime('test') def test_datetime_parsing_empty(self): + ''' Test datetime parsing with empty string. ''' parser = CheddarOutputParser() expected = None @@ -127,6 +141,7 @@ def test_datetime_parsing_empty(self): self.assertEquals(expected, result) def test_error_parser(self): + ''' Test error parser. ''' error_xml = self.load_file('error.xml') expected = { @@ -140,6 +155,7 @@ def test_error_parser(self): self.assertEquals(expected, result) def test_plans_parser(self): + ''' Test plans parser. ''' plans_xml = self.load_file('plans.xml') parser = PlansParser() @@ -193,6 +209,7 @@ def test_plans_parser(self): self.assertEquals(expected, result) def test_plans_parser_with_items(self): + ''' Test plans parser with items. ''' plans_xml = self.load_file('plans_with_items.xml') parser = PlansParser() @@ -322,6 +339,7 @@ def test_plans_parser_with_items(self): self.assertEquals(expected, result) def test_customers_parser_with_no_items(self): + ''' Test customers parser with no items. ''' customers_xml = self.load_file('customers-without-items.xml') parser = CustomersParser() @@ -443,6 +461,7 @@ def test_customers_parser_with_no_items(self): self.assertEquals(expected, result) def test_customers_parser_with_items(self): + ''' Test customers parser with items. ''' customers_xml = self.load_file('customers-with-items.xml') parser = CustomersParser() @@ -606,6 +625,7 @@ def test_customers_parser_with_items(self): self.assertEquals(expected, result) def test_paypal_customer_parse(self): + ''' Test customer parser with paypal customer. ''' customers_xml = self.load_file('paypal_customer.xml') parser = CustomersParser() diff --git a/tests/product_tests.py b/tests/product_tests.py index ae066d0..2266398 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -66,11 +66,13 @@ class ProductTests(unittest.TestCase): } def get_product(self): + ''' Helper method for getting product. ''' product = CheddarProduct(**self.client_defaults) return product def test_repr(self): + ''' Test Product __repr__ method. ''' product = self.get_product() expected = u'CheddarProduct: %s' % product.product_code result = product.__repr__() @@ -78,12 +80,14 @@ def test_repr(self): self.assertEquals(expected, result) def test_instantiate_product(self): + ''' Test product key. ''' product = self.get_product() for key, value in self.client_defaults.items(): self.assertEquals(value, getattr(product.client, key)) def test_get_all_plans(self): + ''' Test product get_all_plans method. ''' product = self.get_product() plans = product.get_all_plans() @@ -101,6 +105,7 @@ def test_get_all_plans(self): self.assertEquals('TRACKED_MONTHLY', tracked_plan.code) def test_get_plan(self): + ''' Test product get_plan method with plan code. ''' product = self.get_product() code = 'PAID_MONTHLY' plan = product.get_plan(code) @@ -108,6 +113,7 @@ def test_get_plan(self): self.assertEquals(code, plan.code) def test_plan_initial_bill_date(self): + ''' Test plan with initial bill date. ''' product = self.get_product() code = 'PAID_MONTHLY' plan = product.get_plan(code) @@ -118,6 +124,7 @@ def test_plan_initial_bill_date(self): self.assertEquals(expected, result) def get_customer(self, **kwargs): + ''' Test Product get_customer method with filtering. ''' customer_data = copy(self.customer_defaults) # We need to make unique customers with the same data. # Cheddar recomends we pass a garbage field. @@ -134,6 +141,7 @@ def get_customer(self, **kwargs): return customer def get_customer_with_items(self, **kwargs): + ''' Test Product get_customer method with items. ''' data = copy(self.paid_defaults) if 'items' in kwargs.keys(): items = kwargs['items'] @@ -150,58 +158,72 @@ def get_customer_with_items(self, **kwargs): @clear_users def test_simple_create_customer(self): + ''' Test Create Customer with only the get_customer helper. ''' self.get_customer() @clear_users def test_create_customer_with_company(self): + ''' Test Create Customer with company name. ''' self.get_customer(company='Test Co') @clear_users def test_create_customer_with_meta_data(self): + ''' Test Create Customer with meta data. ''' self.get_customer(meta_data={'key_1': 'value_1', 'key2': 'value_2'}) @clear_users def test_create_customer_with_true_vat_exempt(self): + ''' Test Create Customer with vat exempt true. ''' self.get_customer(is_vat_exempt=True) @clear_users def test_create_customer_with_false_vat_exempt(self): + ''' Test Create Customer with vat exempt false. ''' self.get_customer(is_vat_exempt=False) @clear_users def test_create_customer_with_vat_number(self): + ''' Test Create Customer with vat number. ''' self.get_customer(vat_number=12345) @clear_users def test_create_customer_with_notes(self): + ''' Test Create Customer with notes. ''' self.get_customer(notes='This is a test note!') @clear_users def test_create_customer_with_first_contact_datetime(self): + ''' Test Create Customer with first contact datetime. ''' self.get_customer(first_contact_datetime=datetime.now()) @clear_users def test_create_customer_with_referer(self): + ''' Test Create Customer with referer. ''' self.get_customer(referer='http://saaspire.com/test.html') @clear_users def test_create_customer_with_campaign_term(self): + ''' Test Create Customer with campaign term. ''' self.get_customer(campaign_term='testing') @clear_users def test_create_customer_with_campaign_name(self): + ''' Test Create Customer with campaign name. ''' self.get_customer(campaign_name='testing') @clear_users def test_create_customer_with_campaign_source(self): + ''' Test Create Customer with campaign source. ''' self.get_customer(campaign_source='testing') @clear_users def test_create_customer_with_campaign_content(self): + ''' Test Create Customer with campaign content. ''' self.get_customer(campaign_content='testing') @clear_users def test_create_customer_with_initial_bill_date(self): + ''' Test Create Customer with initial bill date. ''' initial_bill_date = datetime.utcnow() + timedelta(days=60) customer = self.get_customer(initial_bill_date=initial_bill_date) invoice = customer.subscription.invoices[0] @@ -214,10 +236,12 @@ def test_create_customer_with_initial_bill_date(self): @clear_users def test_create_paid_customer(self): + ''' Test Create Customer with payment. ''' self.get_customer(**self.paid_defaults) @clear_users def test_create_paid_customer_with_charges(self): + ''' Test Create Customer with payment and additional charges. ''' data = copy(self.paid_defaults) charges = [] charges.append({'code': 'test_charge_1', 'each_amount': 2}) @@ -227,6 +251,9 @@ def test_create_paid_customer_with_charges(self): @clear_users def test_create_paid_customer_with_decimal_charges(self): + ''' + Test Create Customer with payment and additional decimal charges. + ''' data = copy(self.paid_defaults) charges = [] charges.append({'code': 'test_charge_1', @@ -237,6 +264,7 @@ def test_create_paid_customer_with_decimal_charges(self): @clear_users def test_create_paid_customer_with_items(self): + ''' Test Create Customer with payment and additional items. ''' data = copy(self.paid_defaults) items = [] items.append({'code': 'MONTHLY_ITEM', 'quantity': 3}) @@ -247,6 +275,10 @@ def test_create_paid_customer_with_items(self): @clear_users def test_create_paid_customer_with_decimal_quantity_items(self): + ''' + Test Create Customer with payment and additional decimal quantity + items. + ''' data = copy(self.paid_defaults) items = [] items.append({'code': 'MONTHLY_ITEM', 'quantity': Decimal('1.23456')}) @@ -257,11 +289,13 @@ def test_create_paid_customer_with_decimal_quantity_items(self): @clear_users def test_create_paypal_customer(self): + ''' Test Create Customer with paypal. ''' data = copy(self.paypal_defaults) self.get_customer(**data) @clear_users def test_update_paypal_customer(self): + ''' Test Update Customer with paypal. ''' data = copy(self.paypal_defaults) customer = self.get_customer(**data) customer.update( @@ -272,6 +306,7 @@ def test_update_paypal_customer(self): @clear_users def test_customer_repr(self): + ''' Test Customer __repr__ method. ''' customer = self.get_customer() expected = 'Customer: Test User (test)' @@ -281,6 +316,7 @@ def test_customer_repr(self): @clear_users def test_subscription_repr(self): + ''' Test Subscription __repr__ method. ''' customer = self.get_customer() subscription = customer.subscription @@ -291,6 +327,7 @@ def test_subscription_repr(self): @clear_users def test_pricing_plan_repr(self): + ''' Test PricingPlan __repr__ method. ''' customer = self.get_customer() subscription = customer.subscription plan = subscription.plan @@ -302,6 +339,7 @@ def test_pricing_plan_repr(self): @clear_users def test_item_repr(self): + ''' Test Item __repr__ method. ''' customer = self.get_customer_with_items() subscription = customer.subscription item = subscription.items['MONTHLY_ITEM'] @@ -330,6 +368,7 @@ def test_get_customers(self): @clear_users def test_get_customer(self): + ''' Test getting a customer by code.. ''' created_customer = self.get_customer() product = self.get_product() @@ -344,6 +383,7 @@ def test_get_customer(self): @clear_users def test_simple_customer_update(self): + ''' Test Update Customer. ''' new_name = 'Different' customer = self.get_customer() product = self.get_product() @@ -357,6 +397,7 @@ def test_simple_customer_update(self): @clear_users @raises(NotFound) def test_delete_customer(self): + ''' Create a Customer and delete that customer. ''' customer = self.get_customer() product = self.get_product() @@ -376,6 +417,10 @@ def test_delete_all_customers(self): self.get_customer(code='test2') product = self.get_product() + # This test fails intermitently. I'm assuming network race condition + # due to creating customers and fetching all customers so quickly. + import time + time.sleep(0.5) fetched_customers = product.get_customers() self.assertEquals(2, len(fetched_customers)) @@ -386,6 +431,7 @@ def test_delete_all_customers(self): @clear_users def test_cancel_subscription(self): + ''' Test cancel subscription. ''' customer = self.get_customer() customer.subscription.cancel() @@ -397,6 +443,7 @@ def test_cancel_subscription(self): self.assertLess(diff, limit) def assert_increment(self, quantity=None): + ''' Helper method for asserting increment in other tests. ''' customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] @@ -414,21 +461,26 @@ def assert_increment(self, quantity=None): @clear_users def test_simple_increment(self): + ''' Test item increment. ''' self.assert_increment() @clear_users def test_int_increment(self): + ''' Test item increment with integer. ''' self.assert_increment(1) @clear_users def test_float_increment(self): + ''' Test item increment with float. ''' self.assert_increment(1.234) @clear_users def test_decimal_increment(self): + ''' Test item increment with decimal. ''' self.assert_increment(Decimal('1.234')) def assert_decrement(self, quantity=None): + ''' Helper method for asserting decrement in other tests. ''' customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] @@ -446,21 +498,28 @@ def assert_decrement(self, quantity=None): @clear_users def test_simple_decrement(self): + ''' Test item decrement. ''' self.assert_decrement() @clear_users def test_int_decrement(self): + ''' Test item decrement with integer. ''' self.assert_decrement(1) @clear_users def test_float_decrement(self): + ''' Test item decrement with float. ''' self.assert_decrement(1.234) @clear_users def test_decimal_decrement(self): + ''' Test item decrement with decimal. ''' self.assert_decrement(Decimal('1.234')) def assert_set(self, quantity): + ''' + Helper method for asserting item quantity has been set as expected. + ''' customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] @@ -476,18 +535,22 @@ def assert_set(self, quantity): @clear_users def test_int_set(self): + ''' Test item set with integer. ''' self.assert_set(1) @clear_users def test_float_set(self): + ''' Test item set with float. ''' self.assert_set(1.234) @clear_users def test_decimal_set(self): + ''' Test item set with decimal. ''' self.assert_set(Decimal('1.234')) def assert_charged(self, code, each_amount, quantity=None, description=None): + ''' Helper method for asserting custom charges as expected. ''' customer = self.get_customer(**self.paid_defaults) product = self.get_product() @@ -526,28 +589,34 @@ def assert_charged(self, code, each_amount, quantity=None, @clear_users def test_add_charge(self): + ''' Test adding a custom charge to an invoice. ''' self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1) @clear_users def test_add_float_charge(self): + ''' Test adding a custom charge to an invoice with float. ''' self.assert_charged(code='TEST-CHARGE', each_amount=2.3, quantity=2) @clear_users def test_add_decimal_charge(self): + ''' Test adding a custom charge to an invoice with decimal. ''' self.assert_charged(code='TEST-CHARGE', each_amount=Decimal('2.3'), quantity=3) @clear_users def test_add_charge_with_descriptions(self): + ''' Test adding a custom charge to an invoice with descriptions. ''' self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1, description="A test charge") @clear_users def test_add_credit(self): + ''' Test adding a custom charge to an invoice as credit. ''' self.assert_charged(code='TEST-CHARGE', each_amount=-1, quantity=1) def assertCharge(self, customer, code, each_amount, quantity, description='', invoice_type=None): + ''' Helper method for asserting custom charge as expected. ''' found_charge = None for invoice in customer.subscription.invoices: if invoice_type is None or invoice['type'] == invoice_type: @@ -561,6 +630,7 @@ def assertCharge(self, customer, code, each_amount, quantity, self.assertEqual(description, found_charge['description']) def assertOneTimeInvoice(self, charges): + ''' Helper method for asserting one time invoice as expected. ''' customer = self.get_customer(**self.paid_defaults) product = self.get_product() @@ -589,6 +659,7 @@ def assertOneTimeInvoice(self, charges): @clear_users def test_add_simple_one_time_invoice(self): + ''' Test adding a one time invoice. ''' charges = [ { 'code': 'immediate-test', @@ -600,6 +671,7 @@ def test_add_simple_one_time_invoice(self): @clear_users def test_add_one_time_invoice_with_description(self): + ''' Test adding a one time invoice with description. ''' charges = [ { 'code': 'immediate-test', @@ -612,6 +684,7 @@ def test_add_one_time_invoice_with_description(self): @clear_users def test_add_one_time_invoice_with_multiple_charges(self): + ''' Test adding a one time invoice with multiple charges. ''' charges = [ { 'code': 'immediate-test', From 043620ff0698b719e40e6c76321121e6741590d7 Mon Sep 17 00:00:00 2001 From: Ryan Johnston Date: Fri, 28 Aug 2015 14:18:32 -0400 Subject: [PATCH 28/53] Add coupon_code to customer create and update methods. With Testing. --- sharpy/parsers.py | 2 ++ sharpy/product.py | 37 +++++++++++++++++++++++-------------- tests/parser_tests.py | 3 +++ tests/product_tests.py | 11 +++++++++++ 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/sharpy/parsers.py b/sharpy/parsers.py index d0f6f60..846fcd7 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -266,6 +266,8 @@ def parse_subscription(self, subscription_element): subscription_element.findtext('canceledDatetime')) subscription['created_datetime'] = self.parse_datetime( subscription_element.findtext('createdDatetime')) + subscription['coupon_code'] = subscription_element.findtext( + 'couponCode') gateway_account_element = subscription_element.find('gatewayAccount') if gateway_account_element is not None: subscription['gateway_account'] = { diff --git a/sharpy/product.py b/sharpy/product.py index 80bb296..188b80a 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -59,8 +59,9 @@ def create_customer(self, code, first_name, last_name, email, plan_code, cc_card_code=None, cc_first_name=None, cc_last_name=None, cc_email=None, cc_company=None, cc_country=None, cc_address=None, cc_city=None, - cc_state=None, cc_zip=None, return_url=None, - cancel_url=None, charges=None, items=None): + cc_state=None, cc_zip=None, coupon_code=None, + return_url=None, cancel_url=None, charges=None, + items=None): data = self.build_customer_post_data(code, first_name, last_name, email, plan_code, company, @@ -75,7 +76,8 @@ def create_customer(self, code, first_name, last_name, email, plan_code, cc_last_name, cc_email, cc_company, cc_country, cc_address, cc_city, cc_state, - cc_zip, return_url, cancel_url) + cc_zip, coupon_code, return_url, + cancel_url) if charges: for i, charge in enumerate(charges): @@ -112,8 +114,9 @@ def build_customer_post_data(self, code=None, first_name=None, cc_last_name=None, cc_email=None, cc_company=None, cc_country=None, cc_address=None, cc_city=None, - cc_state=None, cc_zip=None, return_url=None, - cancel_url=None, bill_date=None): + cc_state=None, cc_zip=None, coupon_code=None, + return_url=None, cancel_url=None, + bill_date=None): data = {} @@ -214,6 +217,9 @@ def build_customer_post_data(self, code=None, first_name=None, if cc_zip: data['subscription[ccZip]'] = cc_zip + if coupon_code: + data['subscription[couponCode]'] = coupon_code + if return_url: data['subscription[returnUrl]'] = return_url @@ -405,7 +411,7 @@ def __init__(self, code, first_name, last_name, email, product, id=None, campaign_medium=None, campaign_term=None, campaign_content=None, campaign_name=None, created_datetime=None, modified_datetime=None, - meta_data=None, subscriptions=None): + coupon_code=None, meta_data=None, subscriptions=None): self.load_data(code=code, first_name=first_name, last_name=last_name, @@ -423,7 +429,7 @@ def __init__(self, code, first_name, last_name, email, product, id=None, campaign_name=campaign_name, created_datetime=created_datetime, modified_datetime=modified_datetime, - meta_data=meta_data, + coupon_code=coupon_code, meta_data=meta_data, subscriptions=subscriptions) super(Customer, self).__init__() @@ -436,7 +442,7 @@ def load_data(self, code, first_name, last_name, email, product, id=None, campaign_medium=None, campaign_term=None, campaign_content=None, campaign_name=None, created_datetime=None, modified_datetime=None, - meta_data=None, subscriptions=None): + coupon_code=None, meta_data=None, subscriptions=None): self.code = code self.id = id self.first_name = first_name @@ -457,6 +463,7 @@ def load_data(self, code, first_name, last_name, email, product, id=None, self.campaign_name = campaign_name self.created = created_datetime self.modified = modified_datetime + self.coupon_code = coupon_code self.meta_data = {} if meta_data: @@ -486,7 +493,7 @@ def update(self, first_name=None, last_name=None, email=None, cc_last_name=None, cc_company=None, cc_email=None, cc_country=None, cc_address=None, cc_city=None, cc_state=None, cc_zip=None, plan_code=None, bill_date=None, - return_url=None, cancel_url=None,): + coupon_code=None, return_url=None, cancel_url=None,): data = self.product.build_customer_post_data( first_name=first_name, last_name=last_name, email=email, @@ -500,8 +507,8 @@ def update(self, first_name=None, last_name=None, email=None, cc_first_name=cc_first_name, cc_last_name=cc_last_name, cc_company=cc_company, cc_email=cc_email, cc_country=cc_country, cc_address=cc_address, cc_city=cc_city, cc_state=cc_state, - cc_zip=cc_zip, bill_date=bill_date, return_url=return_url, - cancel_url=cancel_url,) + cc_zip=cc_zip, bill_date=bill_date, coupon_code=coupon_code, + return_url=return_url, cancel_url=cancel_url,) path = 'customers/edit' params = {'code': self.code} @@ -599,7 +606,7 @@ def __init__(self, id, gateway_token, cc_first_name, cc_last_name, canceled_datetime=None, created_datetime=None, plans=None, invoices=None, items=None, gateway_account=None, cancel_reason=None, cancel_type=None, cc_email=None, - redirect_url=None): + coupon_code=None, redirect_url=None): self.load_data(id=id, gateway_token=gateway_token, cc_first_name=cc_first_name, @@ -615,7 +622,7 @@ def __init__(self, id, gateway_token, cc_first_name, cc_last_name, invoices=invoices, items=items, gateway_account=gateway_account, cancel_reason=cancel_reason, cancel_type=cancel_type, - redirect_url=redirect_url) + coupon_code=coupon_code, redirect_url=redirect_url) super(Subscription, self).__init__() @@ -624,7 +631,8 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, cc_email=None, canceled_datetime=None, created_datetime=None, plans=None, invoices=None, items=None, gateway_account=None, - cancel_reason=None, cancel_type=None, redirect_url=None): + cancel_reason=None, cancel_type=None, coupon_code=None, + redirect_url=None): self.id = id self.gateway_token = gateway_token @@ -647,6 +655,7 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, self.gateway_account = gateway_account self.cancel_type = cancel_type self.cancel_reason = cancel_reason + self.coupon_code = coupon_code self.redirect_url = redirect_url # Organize item data into something more useful diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 9ab3a37..ae00e68 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -389,6 +389,7 @@ def test_customers_parser_with_no_items(self): 'cancel_type': None, 'canceled_datetime': None, 'cc_address': '', + 'coupon_code': None, 'cc_city': '', 'cc_company': '', 'cc_country': '', @@ -495,6 +496,7 @@ def test_customers_parser_with_items(self): 'cancel_type': None, 'canceled_datetime': None, 'cc_address': '123 Something St', + 'coupon_code': None, 'cc_city': 'Someplace', 'cc_company': 'Some Co LLC', 'cc_country': 'United States', @@ -660,6 +662,7 @@ def test_paypal_customer_parse(self): 'canceled_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), 'cc_address': '', + 'coupon_code': None, 'cc_city': '', 'cc_company': '', 'cc_country': '', diff --git a/tests/product_tests.py b/tests/product_tests.py index 2266398..95d4f20 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -249,6 +249,13 @@ def test_create_paid_customer_with_charges(self): data['charges'] = charges self.get_customer(**data) + @clear_users + def test_create_paid_customer_with_coupon_code(self): + ''' Test Create Customer with payment and coupon codes. ''' + data = copy(self.paid_defaults) + data.update({'coupon_code': 'COUPON'}) + self.get_customer(**data) + @clear_users def test_create_paid_customer_with_decimal_charges(self): ''' @@ -363,6 +370,10 @@ def test_get_customers(self): self.get_customer(**customer2_data) product = self.get_product() + # This test fails intermitently. I'm assuming network race condition + # due to creating customers and fetching all customers so quickly. + import time + time.sleep(0.5) fetched_customers = product.get_customers() self.assertEquals(2, len(fetched_customers)) From 186f6d6375aa81d39b628aaaefc1d3cc064b32b4 Mon Sep 17 00:00:00 2001 From: David Galitsky Date: Tue, 15 Mar 2016 13:53:03 -0400 Subject: [PATCH 29/53] added plans to Promotion and PromotionParser. See #3141 --- sharpy/parsers.py | 10 ++++++++++ sharpy/product.py | 9 ++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/sharpy/parsers.py b/sharpy/parsers.py index 846fcd7..ff0fb78 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -400,6 +400,8 @@ def parse_promotion(self, promotion_element): promotion['incentives'] = self.parse_incentives( promotion_element.find('incentives')) + import ipdb; ipdb.set_trace() + promotion['plans'] = self.parse_plans(promotion_element.find('plans')) promotion['coupons'] = self.parse_coupons( promotion_element.find('coupons')) @@ -426,6 +428,14 @@ def parse_incentive(self, incentive_element): return incentive + def parse_plans(self, plans_element): + plans = [] + + if plans_element is not None: + for plan_element in plans_element: + plans.append(plan_element.findtext('code')) + return plans + def parse_coupons(self, coupons_element): coupons = [] diff --git a/sharpy/product.py b/sharpy/product.py index 188b80a..2d4c8b1 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -821,11 +821,12 @@ def set(self, quantity): class Promotion(object): def __init__(self, id=None, code=None, name=None, description=None, - created_datetime=None, incentives=None, coupons=None): + created_datetime=None, incentives=None, plans=None, + coupons=None): self.load_data(code=code, id=id, name=name, description=description, created_datetime=created_datetime, - incentives=incentives, coupons=coupons) + incentives=incentives, plans=plans, coupons=coupons) super(Promotion, self).__init__() @@ -836,7 +837,8 @@ def __unicode__(self): return u'{0} ({1})'.format(self.name, self.code) def load_data(self, id=None, code=None, name=None, description=None, - created_datetime=None, incentives=None, coupons=None): + created_datetime=None, incentives=None, plans=None, + coupons=None): self.code = code self.id = id @@ -844,6 +846,7 @@ def load_data(self, id=None, code=None, name=None, description=None, self.description = description self.created = created_datetime + self.plans = plans self.incentives = incentives self.coupons = coupons From 359c0e80b0f4d5af428b2dd81f2c097903911bbb Mon Sep 17 00:00:00 2001 From: David Galitsky Date: Tue, 15 Mar 2016 14:02:47 -0400 Subject: [PATCH 30/53] remove ipdb trace. See #0000 --- sharpy/parsers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sharpy/parsers.py b/sharpy/parsers.py index ff0fb78..d9760ab 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -400,7 +400,6 @@ def parse_promotion(self, promotion_element): promotion['incentives'] = self.parse_incentives( promotion_element.find('incentives')) - import ipdb; ipdb.set_trace() promotion['plans'] = self.parse_plans(promotion_element.find('plans')) promotion['coupons'] = self.parse_coupons( promotion_element.find('coupons')) From b742d612884c7e2f3c7f03bfdb9b407542315b67 Mon Sep 17 00:00:00 2001 From: David Galitsky Date: Thu, 17 Mar 2016 11:31:40 -0400 Subject: [PATCH 31/53] got tests to run on sharpy. Added new test to parse promotions. See #3141 --- setup.cfg | 1 - tests/client_tests.py | 2 +- tests/files/plans.xml | 11 ++++--- tests/files/promotions.xml | 62 ++++++++++++++++++++++++++++++++++++ tests/parser_tests.py | 50 +++++++++++++++++++++++++++++ tests/product_tests.py | 32 +++++++++---------- tests/testing_tools/utils.py | 2 +- 7 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 tests/files/promotions.xml diff --git a/setup.cfg b/setup.cfg index 30dfd1b..d765f5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,4 @@ [nosetests] -tc-file=tests/config.ini detailed-errors=1 with-coverage=1 cover-package=sharpy diff --git a/tests/client_tests.py b/tests/client_tests.py index 370da62..d7eca68 100644 --- a/tests/client_tests.py +++ b/tests/client_tests.py @@ -108,7 +108,7 @@ def test_make_request_access_denied(self): client = self.get_client(username=bad_username) client.make_request(path) - @raises(NotFound) + @raises(BadRequest) def test_make_request_bad_request(self): ''' Attempt to grab the plans without adding /get to the url. ''' path = 'plans' diff --git a/tests/files/plans.xml b/tests/files/plans.xml index 0c7f19e..0288499 100644 --- a/tests/files/plans.xml +++ b/tests/files/plans.xml @@ -1,5 +1,6 @@ - - + + + Free Monthly A free monthly plan 1 @@ -16,7 +17,7 @@ FREE_MONTHLY_RECURRING 0.00 2011-01-07T20:46:43+00:00 - + Paid Monthly @@ -34,5 +35,5 @@ PAID_MONTHLY_RECURRING 20.00 2011-01-07T21:05:42+00:00 - - \ No newline at end of file + + diff --git a/tests/files/promotions.xml b/tests/files/promotions.xml new file mode 100644 index 0000000..254fafc --- /dev/null +++ b/tests/files/promotions.xml @@ -0,0 +1,62 @@ + + + 10% off initial yr UNLIMITED + Get your first year of unlimited, 10% off! + 2016-03-14T16:09:15+00:00 + + + UNLIMITED_ANNUAL + Unlimited Annual + + + + + percentage + 10 + 1 + + + + + 10ANNUAL + 5 + 2016-03-31T00:00:00+00:00 + 2016-03-14T16:09:15+00:00 + + + + + 20% off annual + + 2015-11-30T21:13:14+00:00 + + + UNLIMITED_ANNUAL + Unlimited Annual + + + BUSINESS_PLUS_ANNUAL + Business Plus Annual + + + BUSINESS_ANNUAL + Business Annual + + + + + percentage + 20 + 0 + + + + + 20OFF + 0 + + 2015-11-30T21:13:15+00:00 + + + + diff --git a/tests/parser_tests.py b/tests/parser_tests.py index ae00e68..3d0b399 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -11,6 +11,7 @@ from sharpy.parsers import parse_error from sharpy.parsers import PlansParser from sharpy.parsers import CustomersParser +from sharpy.parsers import PromotionsParser class ParserTests(unittest.TestCase): @@ -154,6 +155,55 @@ def test_error_parser(self): self.assertEquals(expected, result) + def test_promotions_parser(self): + ''' Tests promotions parser. ''' + promotions_xml = self.load_file('promotions.xml') + parser = PromotionsParser() + + expected = [ + { + 'description': 'Get your first year of unlimited, 10% off!', + 'created_datetime': datetime(2016, 3, 14, 16, 9, 15, tzinfo=tzutc()), + 'incentives': [{'created_datetime': None, + 'percentage': '10', + 'months': '1', + 'type': 'percentage', + 'id': '88350e9b-69f6-4ce8-a542-b82e03329cff'}], + 'name': '10% off initial yr UNLIMITED', + 'id': 'c14a8154-b007-409d-851a-1e9aac9da14b', + 'coupons': [{'created_datetime': datetime(2016, 3, 14, 16, 9, 15, tzinfo=tzutc()), + 'expiration_datetime': datetime(2016, 3, 31, 0, 0, tzinfo=tzutc()), + 'code': '10ANNUAL', + 'id': '45c156c1-3149-4d6f-b4cf-9ad5ad8b6a5d', + 'max_redemptions': '5'}], + 'plans': ['UNLIMITED_ANNUAL'] + }, + { + 'description': '', + 'created_datetime': datetime(2015, 11, 30, 21, 13, 14, tzinfo=tzutc()), + 'incentives': [{'created_datetime': None, + 'percentage': '20', + 'months': '0', + 'type': 'percentage', + 'id': 'd3f33004-4398-4a82-b354-c1ea8732c55d'}], + 'name': '20% off annual', + 'id': 'a0b3d969-028b-400d-a518-701c34354f6d', + 'coupons': [{'created_datetime': datetime(2015, 11, 30, 21, 13, 15, tzinfo=tzutc()), + 'expiration_datetime': None, + 'code': '20OFF', + 'id': '7f1924b9-7d7f-4fd8-b751-8d0babf1431b', + 'max_redemptions': '0'}], + 'plans': ['UNLIMITED_ANNUAL', 'BUSINESS_PLUS_ANNUAL', 'BUSINESS_ANNUAL'] + } + ] + + result = parser.parse_xml(promotions_xml) + import pprint + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(result) + + self.assertEquals(expected, result) + def test_plans_parser(self): ''' Test plans parser. ''' plans_xml = self.load_file('plans.xml') diff --git a/tests/product_tests.py b/tests/product_tests.py index 95d4f20..9afcc54 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -294,22 +294,22 @@ def test_create_paid_customer_with_decimal_quantity_items(self): data['plan_code'] = 'TRACKED_MONTHLY' self.get_customer(**data) - @clear_users - def test_create_paypal_customer(self): - ''' Test Create Customer with paypal. ''' - data = copy(self.paypal_defaults) - self.get_customer(**data) - - @clear_users - def test_update_paypal_customer(self): - ''' Test Update Customer with paypal. ''' - data = copy(self.paypal_defaults) - customer = self.get_customer(**data) - customer.update( - method='paypal', - return_url='http://example.com/update-success/', - cancel_url='http://example.com/update-cancel/', - ) + #@clear_users + #def test_create_paypal_customer(self): + # ''' Test Create Customer with paypal. ''' + # data = copy(self.paypal_defaults) + # self.get_customer(**data) + + #@clear_users + #def test_update_paypal_customer(self): + # ''' Test Update Customer with paypal. ''' + # data = copy(self.paypal_defaults) + # customer = self.get_customer(**data) + # customer.update( + # method='paypal', + # return_url='http://example.com/update-success/', + # cancel_url='http://example.com/update-cancel/', + # ) @clear_users def test_customer_repr(self): diff --git a/tests/testing_tools/utils.py b/tests/testing_tools/utils.py index 68cb0c8..909a281 100644 --- a/tests/testing_tools/utils.py +++ b/tests/testing_tools/utils.py @@ -1,7 +1,7 @@ import httplib2 from time import time -from testconfig import config +from tests.testconfig import config def clear_users(): From 5a6e57bd4408475aba7475cd105867e571785492 Mon Sep 17 00:00:00 2001 From: Taylor Brazelton Date: Thu, 7 Apr 2016 11:49:17 -0400 Subject: [PATCH 32/53] Updated version number. --- sharpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharpy/__init__.py b/sharpy/__init__.py index c07c373..4e4f145 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 9) +VERSION = (0, 9, 1) From 3b818bb7170c585a14d355fec5fe6a3ea56586bd Mon Sep 17 00:00:00 2001 From: Taylor Brazelton Date: Thu, 7 Apr 2016 11:49:58 -0400 Subject: [PATCH 33/53] updated version for release. --- sharpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharpy/__init__.py b/sharpy/__init__.py index 4e4f145..49d36f5 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 9, 1) +VERSION = (0, 9, 3) From cf21319cac87347cd999e517c2b352c8eddb1d81 Mon Sep 17 00:00:00 2001 From: Jessica Luke Date: Tue, 14 Jun 2016 11:57:46 -0400 Subject: [PATCH 34/53] Clean credit card info from data that is logged in debug mode --- sharpy/client.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sharpy/client.py b/sharpy/client.py index 980d204..d6eafbe 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -87,6 +87,7 @@ def make_request(self, path, params=None, data=None, method=None): method = method or 'GET' body = None headers = {} + cleaned_data = None if data: method = 'POST' @@ -96,9 +97,16 @@ def make_request(self, path, params=None, data=None, method=None): 'application/x-www-form-urlencoded; charset=UTF-8', } + # Clean credit card info from when the request gets logged + # (remove ccv and only show last four of card num) + cleaned_data = data.copy() + del cleaned_data['subscription[ccCardCode]'] + ccNum = cleaned_data['subscription[ccNumber]'] + cleaned_data['subscription[ccNumber]'] = ccNum[-4:] + + client_log.debug('Request Method: %s' % method) - client_log.debug('Request Body(Data): %s' % data) - client_log.debug('Request Body(Raw): %s' % body) + client_log.debug('Request Body (Cleaned Data): %s' % cleaned_data) # Setup http client h = httplib2.Http(cache=self.cache, timeout=self.timeout) From 00ca5ae70ce0b16f43af97d7230d34e732ca5cd6 Mon Sep 17 00:00:00 2001 From: Jessica Luke Date: Tue, 14 Jun 2016 14:12:17 -0400 Subject: [PATCH 35/53] Bump version --- sharpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharpy/__init__.py b/sharpy/__init__.py index 49d36f5..8ca9f33 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 9, 3) +VERSION = (0, 9, 4) From 357d8ed9769ca3d1ee579b5559dddfa1dad74a48 Mon Sep 17 00:00:00 2001 From: David Galitsky Date: Sat, 18 Jun 2016 14:34:01 -0400 Subject: [PATCH 36/53] update version number --- sharpy/__init__.py | 2 +- sharpy/client.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sharpy/__init__.py b/sharpy/__init__.py index 8ca9f33..5d35608 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 9, 4) +VERSION = (0, 9, 5) diff --git a/sharpy/client.py b/sharpy/client.py index d6eafbe..d2b3770 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -100,7 +100,10 @@ def make_request(self, path, params=None, data=None, method=None): # Clean credit card info from when the request gets logged # (remove ccv and only show last four of card num) cleaned_data = data.copy() - del cleaned_data['subscription[ccCardCode]'] + try: + del cleaned_data['subscription[ccCardCode]'] + except KeyError: + pass ccNum = cleaned_data['subscription[ccNumber]'] cleaned_data['subscription[ccNumber]'] = ccNum[-4:] From c66e39015b67be8d95b9e522f5c774d66aa46d9c Mon Sep 17 00:00:00 2001 From: David Galitsky Date: Sat, 18 Jun 2016 22:07:34 -0400 Subject: [PATCH 37/53] added more under try/except. On exception make credit card None. --- sharpy/client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sharpy/client.py b/sharpy/client.py index d2b3770..579faee 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -102,11 +102,10 @@ def make_request(self, path, params=None, data=None, method=None): cleaned_data = data.copy() try: del cleaned_data['subscription[ccCardCode]'] + ccNum = cleaned_data['subscription[ccNumber]'] + cleaned_data['subscription[ccNumber]'] = ccNum[-4:] except KeyError: - pass - ccNum = cleaned_data['subscription[ccNumber]'] - cleaned_data['subscription[ccNumber]'] = ccNum[-4:] - + cleaned_data['subscription[ccNumber]'] = None client_log.debug('Request Method: %s' % method) client_log.debug('Request Body (Cleaned Data): %s' % cleaned_data) From 05224f8ef7c20848dc81312bad7f3af545280606 Mon Sep 17 00:00:00 2001 From: David Galitsky Date: Sat, 18 Jun 2016 22:08:11 -0400 Subject: [PATCH 38/53] updated version number --- sharpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharpy/__init__.py b/sharpy/__init__.py index 5d35608..73dbc55 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 9, 5) +VERSION = (0, 9, 6) From 8154d0ee337a5104ccb6b4152ae5bbac977d9345 Mon Sep 17 00:00:00 2001 From: Jessica Luke Date: Mon, 20 Jun 2016 10:45:16 -0400 Subject: [PATCH 39/53] Check that cc info is in the request before removing it --- sharpy/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sharpy/client.py b/sharpy/client.py index 579faee..1a2ea97 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -100,12 +100,11 @@ def make_request(self, path, params=None, data=None, method=None): # Clean credit card info from when the request gets logged # (remove ccv and only show last four of card num) cleaned_data = data.copy() - try: + if 'subscription[ccCardCode]' in cleaned_data: del cleaned_data['subscription[ccCardCode]'] + if 'subscription[ccNumber]' in cleaned_data: ccNum = cleaned_data['subscription[ccNumber]'] cleaned_data['subscription[ccNumber]'] = ccNum[-4:] - except KeyError: - cleaned_data['subscription[ccNumber]'] = None client_log.debug('Request Method: %s' % method) client_log.debug('Request Body (Cleaned Data): %s' % cleaned_data) From fd10ef433fa5993d0afd6ef408b15e38493f5e85 Mon Sep 17 00:00:00 2001 From: Jessica Hayley Date: Mon, 20 Jun 2016 10:52:09 -0400 Subject: [PATCH 40/53] Update version number --- sharpy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharpy/__init__.py b/sharpy/__init__.py index 73dbc55..6c6b026 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 9, 6) +VERSION = (0, 9, 7) From 1017d604a77f1a0a028f93f72be812a71857af4f Mon Sep 17 00:00:00 2001 From: Tony Spelde Date: Thu, 31 May 2018 13:27:57 -0400 Subject: [PATCH 41/53] convert to python3 --- Sharpy.egg-info/PKG-INFO | 86 +++ Sharpy.egg-info/SOURCES.txt | 43 ++ Sharpy.egg-info/dependency_links.txt | 1 + Sharpy.egg-info/requires.txt | 3 + Sharpy.egg-info/top_level.txt | 1 + build/lib/sharpy/__init__.py | 1 + build/lib/sharpy/client.py | 144 +++++ build/lib/sharpy/exceptions.py | 69 +++ build/lib/sharpy/parsers.py | 458 ++++++++++++++ build/lib/sharpy/product.py | 855 +++++++++++++++++++++++++++ dist/Sharpy-0.9.7-py2.7.egg | Bin 0 -> 30491 bytes dist/Sharpy-0.9.7-py3.6.egg | Bin 0 -> 30436 bytes getCheddarCustomers.py | 19 + setup.py | 2 +- sharpy/client.py | 15 +- sharpy/parsers.py | 5 +- sharpy/product.py | 23 +- 17 files changed, 1702 insertions(+), 23 deletions(-) create mode 100644 Sharpy.egg-info/PKG-INFO create mode 100644 Sharpy.egg-info/SOURCES.txt create mode 100644 Sharpy.egg-info/dependency_links.txt create mode 100644 Sharpy.egg-info/requires.txt create mode 100644 Sharpy.egg-info/top_level.txt create mode 100644 build/lib/sharpy/__init__.py create mode 100644 build/lib/sharpy/client.py create mode 100644 build/lib/sharpy/exceptions.py create mode 100644 build/lib/sharpy/parsers.py create mode 100644 build/lib/sharpy/product.py create mode 100644 dist/Sharpy-0.9.7-py2.7.egg create mode 100644 dist/Sharpy-0.9.7-py3.6.egg create mode 100644 getCheddarCustomers.py diff --git a/Sharpy.egg-info/PKG-INFO b/Sharpy.egg-info/PKG-INFO new file mode 100644 index 0000000..083aefd --- /dev/null +++ b/Sharpy.egg-info/PKG-INFO @@ -0,0 +1,86 @@ +Metadata-Version: 1.1 +Name: Sharpy +Version: 0.9.7 +Summary: Python client for the Cheddar Getter API (http://cheddargetter.com). +Home-page: https://github.com/Saaspire/sharpy +Author: Sean O'Connor +Author-email: sean@saaspire.com +License: BSD +Description: ====== + Sharpy + ====== + + Sharpy is a client for the Cheddar Getter (https://cheddargetter.com/) API. + Cheddar Getter is a great service for handling recurring and usage based + billing. + + There are some existing python Cheddar Getter clients but they have + significant licensing problems, packaging problems, bugs, and are only partial + implementations of the Cheddar Getter API. + + Sharpy offers a number of advantages: + + * Clear and simple BSD license. + * Both a high and low level API - Work with cheddar the way you want to. + * 100% test coverage. + * Proper packaging - Sharpy can be installed via easy_install and PIP. + * Implements almost all of the Cheddar Getter API (See TODOs below). + * Will have complete documentation soon. + + That all being said, sharpy is still very new and is likely to undergo some + significant API changes in the near future. The code should be fairly safe + to use as long as you understand that future releases may not be backwards + compatible. + + Getting Started + =============== + + To get started with Sharpy, simply install it like you would any other python + package + + .. code:: + + pip install sharpy + + Optionally, you can also install `lxml `_ on your + system for faster XML parsing. + + Once you have sharpy installed, checkout our `docs `_ + on how to use the library. + + Documentation + ============= + + Sharpy's documentation is available at `ReadTheDocs + `_ or in the ``docs`` directory of the project. + + Code + ==== + + You can checkout and download Sharpy's latest code at `Github + `_. + + Installing elementtree for Development and Unit Testing + ======================================================= + When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. + + .. code:: + + wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip + unzip elementtree-1.2.6-20050316.zip + cd elementtree-1.2.6-20050316/ + pip install . + + TODOs + ===== + + * Flesh out the documentation to cover the full API. + * Add support for the various filtering options in the `get_customers` call. + +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/Sharpy.egg-info/SOURCES.txt b/Sharpy.egg-info/SOURCES.txt new file mode 100644 index 0000000..cac2056 --- /dev/null +++ b/Sharpy.egg-info/SOURCES.txt @@ -0,0 +1,43 @@ +LICENSE.txt +MANIFEST.in +README.rst +dev-requirements.txt +setup.cfg +setup.py +Sharpy.egg-info/PKG-INFO +Sharpy.egg-info/SOURCES.txt +Sharpy.egg-info/dependency_links.txt +Sharpy.egg-info/requires.txt +Sharpy.egg-info/top_level.txt +docs/conf.py +docs/development.rst +docs/examples.rst +docs/high_level_api.rst +docs/index.rst +docs/examples/customer_creation/charges.py +docs/examples/customer_creation/free.py +docs/examples/customer_creation/items.py +docs/examples/customer_creation/optional.py +docs/examples/customer_creation/paid.py +docs/examples/customer_creation/paypal.py +docs/examples/customer_fetch/all_customers.py +docs/examples/customer_fetch/get_customer.py +sharpy/__init__.py +sharpy/client.py +sharpy/exceptions.py +sharpy/parsers.py +sharpy/product.py +tests/__init__.py +tests/client_tests.py +tests/parser_tests.py +tests/product_tests.py +tests/files/customers-with-items.xml +tests/files/customers-without-items.xml +tests/files/error.xml +tests/files/paypal_customer.xml +tests/files/plans.xml +tests/files/plans_with_items.xml +tests/files/promotions.xml +tests/testing_tools/__init__.py +tests/testing_tools/decorators.py +tests/testing_tools/utils.py \ No newline at end of file diff --git a/Sharpy.egg-info/dependency_links.txt b/Sharpy.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Sharpy.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/Sharpy.egg-info/requires.txt b/Sharpy.egg-info/requires.txt new file mode 100644 index 0000000..ecbed54 --- /dev/null +++ b/Sharpy.egg-info/requires.txt @@ -0,0 +1,3 @@ +httplib2 +elementtree +python-dateutil<2.0 diff --git a/Sharpy.egg-info/top_level.txt b/Sharpy.egg-info/top_level.txt new file mode 100644 index 0000000..814d393 --- /dev/null +++ b/Sharpy.egg-info/top_level.txt @@ -0,0 +1 @@ +sharpy diff --git a/build/lib/sharpy/__init__.py b/build/lib/sharpy/__init__.py new file mode 100644 index 0000000..6c6b026 --- /dev/null +++ b/build/lib/sharpy/__init__.py @@ -0,0 +1 @@ +VERSION = (0, 9, 7) diff --git a/build/lib/sharpy/client.py b/build/lib/sharpy/client.py new file mode 100644 index 0000000..7e6c7a2 --- /dev/null +++ b/build/lib/sharpy/client.py @@ -0,0 +1,144 @@ +import base64 +import logging +from urllib.parse import urlencode +from dateutil.tz import tzutc +import httplib2 + +from sharpy.exceptions import AccessDenied +from sharpy.exceptions import BadRequest +from sharpy.exceptions import CheddarError +from sharpy.exceptions import CheddarFailure +from sharpy.exceptions import NaughtyGateway +from sharpy.exceptions import NotFound +from sharpy.exceptions import PreconditionFailed +from sharpy.exceptions import UnprocessableEntity + +client_log = logging.getLogger('SharpyClient') + + +class Client(object): + default_endpoint = 'https://cheddargetter.com/xml' + + def __init__(self, username, password, product_code, cache=None, + timeout=None, endpoint=None): + ''' + username - Your cheddargetter username (probably an email address) + password - Your cheddargetter password + product_code - The product code for the product you want to work with + cache - A file system path or an object which implements the httplib2 + cache API (optional) + timeout - Socket level timout in seconds (optional) + endpoint - An alternate API endpoint (optional) + ''' + self.username = username + self.password = password + self.product_code = product_code + self.endpoint = endpoint or self.default_endpoint + self.cache = cache + self.timeout = timeout + + super(Client, self).__init__() + + def build_url(self, path, params=None): + ''' + Constructs the url for a cheddar API resource + ''' + url = '%s/%s/productCode/%s' % ( + self.endpoint, + path, + self.product_code, + ) + if params: + for key, value in list(params.items()): + url = '%s/%s/%s' % (url, key, value) + + return url + + def format_datetime(self, to_format): + if to_format == 'now': + str_dt = to_format + else: + if getattr(to_format, 'tzinfo', None) is not None: + utc_value = to_format.astimezone(tzutc()) + else: + utc_value = to_format + str_dt = utc_value.strftime('%Y-%m-%dT%H:%M:%S+00:00') + return str_dt + + def format_date(self, to_format): + if to_format == 'now': + str_dt = to_format + else: + if getattr(to_format, 'tzinfo', None) is not None: + utc_value = to_format.astimezone(tzutc()) + else: + utc_value = to_format + str_dt = utc_value.strftime('%Y-%m-%d') + return str_dt + + def make_request(self, path, params=None, data=None, method=None): + ''' + Makes a request to the cheddar api using the authentication and + configuration settings available. + ''' + # Setup values + url = self.build_url(path, params) + client_log.debug('Requesting: %s' % url) + method = method or 'GET' + body = None + headers = {} + cleaned_data = None + + if data: + method = 'POST' + body = urlencode(data) + headers = { + 'content-type': + 'application/x-www-form-urlencoded; charset=UTF-8', + } + + # Clean credit card info from when the request gets logged + # (remove ccv and only show last four of card num) + cleaned_data = data.copy() + if 'subscription[ccCardCode]' in cleaned_data: + del cleaned_data['subscription[ccCardCode]'] + if 'subscription[ccNumber]' in cleaned_data: + ccNum = cleaned_data['subscription[ccNumber]'] + cleaned_data['subscription[ccNumber]'] = ccNum[-4:] + + client_log.debug('Request Method: %s' % method) + client_log.debug('Request Body (Cleaned Data): %s' % cleaned_data) + + # Setup http client + h = httplib2.Http(cache=self.cache, timeout=self.timeout) + # Skip the normal http client behavior and send auth headers + # immediately to save an http request. + headers['Authorization'] = "Basic %s" % base64.standard_b64encode( + self.username + ':' + self.password).strip() + + # Make request + response, content = h.request(url, method, body=body, headers=headers) + status = response.status + client_log.debug('Response Status: %d' % status) + client_log.debug('Response Content: %s' % content) + if status != 200 and status != 302: + exception_class = CheddarError + if status == 401: + exception_class = AccessDenied + elif status == 400: + exception_class = BadRequest + elif status == 404: + exception_class = NotFound + elif status == 412: + exception_class = PreconditionFailed + elif status == 500: + exception_class = CheddarFailure + elif status == 502: + exception_class = NaughtyGateway + elif status == 422: + exception_class = UnprocessableEntity + + raise exception_class(response, content) + + response.content = content + return response diff --git a/build/lib/sharpy/exceptions.py b/build/lib/sharpy/exceptions.py new file mode 100644 index 0000000..ab2cf17 --- /dev/null +++ b/build/lib/sharpy/exceptions.py @@ -0,0 +1,69 @@ + + +class CheddarError(Exception): + "Base class for exceptions returned by cheddar" + + def __init__(self, response, content, *args, **kwargs): + # Importing in method to break circular dependecy + from sharpy.parsers import parse_error + + super(CheddarError, self).__init__(*args, **kwargs) + error_info = parse_error(content) + self.response = response + self.error_info = error_info + + def __str__(self): + return '%s (%s) %s - %s' % ( + self.response.status, + self.error_info['aux_code'], + self.response.reason, + self.error_info['message'], + ) + + +class AccessDenied(CheddarError): + "A request to cheddar returned a status code of 401" + pass + + +class BadRequest(CheddarError): + "A request to cheddar was invalid in some way" + pass + + +class NotFound(CheddarError): + "A request to chedder was made for a resource which doesn't exist" + pass + + +class CheddarFailure(CheddarError): + "A request to cheddar encountered an unexpected error on the cheddar side" + pass + + +class PreconditionFailed(CheddarError): + "A request to cheddar was made but failed CG's validation in some way." + pass + + +class NaughtyGateway(CheddarError): + """ + Cheddar either couldn't contact the gateway or the gateway did something + very unexpected. + """ + pass + + +class UnprocessableEntity(CheddarError): + """ + An error occurred during processing. Please fix the error and try again. + """ + pass + + +class ParseError(Exception): + """ + Sharpy recieved unknown output from cheddar and doesn't know what + to do with it. + """ + pass diff --git a/build/lib/sharpy/parsers.py b/build/lib/sharpy/parsers.py new file mode 100644 index 0000000..d9760ab --- /dev/null +++ b/build/lib/sharpy/parsers.py @@ -0,0 +1,458 @@ +from decimal import Decimal, InvalidOperation +import logging + +from dateutil import parser as date_parser + +try: + from lxml.etree import XML +except ImportError: + from elementtree.ElementTree import XML + +from sharpy.exceptions import ParseError + +client_log = logging.getLogger('SharpyClient') + + +def parse_error(xml_str): + error = {} + doc = XML(xml_str) + if doc.tag == 'error': + elem = doc + elif doc.tag == 'customers': + elem = doc.find('.//error') + else: + raise Exception("Can't find error element in '%s'" % xml_str) + client_log.debug(elem) + error['id'] = elem.attrib['id'] + error['code'] = elem.attrib['code'] + error['aux_code'] = elem.attrib['auxCode'] + error['message'] = elem.text + + return error + + +class CheddarOutputParser(object): + ''' + A utility class for parsing the various datatypes returned by the + cheddar api. + ''' + def parse_bool(self, content): + if content == '' and content is not None: + value = None + elif content == '1': + value = True + elif content == '0': + value = False + else: + raise ParseError("Can't parse '%s' as a bool." % content) + + return value + + def parse_int(self, content): + value = None + if content != '' and content is not None: + try: + value = int(content) + except ValueError: + raise ParseError("Can't parse '%s' as an int." % content) + + return value + + def parse_decimal(self, content): + value = None + if content != '' and content is not None: + try: + value = Decimal(content) + except InvalidOperation: + raise ParseError("Can't parse '%s' as a decimal." % content) + + return value + + def parse_datetime(self, content): + value = None + if content: + try: + value = date_parser.parse(content) + except ValueError: + raise ParseError("Can't parse '%s' as a datetime." % content) + + return value + + +class PlansParser(CheddarOutputParser): + ''' + A utility class for parsing cheddar's xml output for pricing plans. + ''' + def parse_xml(self, xml_str): + plans = [] + plans_xml = XML(xml_str) + for plan_xml in plans_xml: + plan = self.parse_plan(plan_xml) + plans.append(plan) + + return plans + + def parse_plan(self, plan_element): + plan = {} + plan['id'] = plan_element.attrib['id'] + plan['code'] = plan_element.attrib['code'] + plan['name'] = plan_element.findtext('name') + plan['description'] = plan_element.findtext('description') + plan['is_active'] = self.parse_bool(plan_element.findtext('isActive')) + plan['is_free'] = self.parse_bool(plan_element.findtext('isFree')) + plan['trial_days'] = self.parse_int(plan_element.findtext('trialDays')) + plan['initial_bill_count'] = self.parse_int(plan_element.findtext( + 'initialBillCount')) + plan['initial_bill_count_unit'] = plan_element.findtext( + 'initialBillCountUnit') + plan['billing_frequency'] = plan_element.findtext('billingFrequency') + plan['billing_frequency_per'] = plan_element.findtext( + 'billingFrequencyPer') + plan['billing_frequency_unit'] = plan_element.findtext( + 'billingFrequencyUnit') + plan['billing_frequency_quantity'] = self.parse_int( + plan_element.findtext('billingFrequencyQuantity')) + plan['setup_charge_code'] = plan_element.findtext('setupChargeCode') + plan['setup_charge_amount'] = self.parse_decimal( + plan_element.findtext('setupChargeAmount')) + plan['recurring_charge_code'] = plan_element.findtext( + 'recurringChargeCode') + plan['recurring_charge_amount'] = self.parse_decimal( + plan_element.findtext('recurringChargeAmount')) + plan['created_datetime'] = self.parse_datetime( + plan_element.findtext('createdDatetime')) + + plan['items'] = self.parse_plan_items(plan_element.find('items')) + + return plan + + def parse_plan_items(self, items_element): + items = [] + + if items_element is not None: + for item_element in items_element: + items.append(self.parse_plan_item(item_element)) + + return items + + def parse_plan_item(self, item_element): + item = {} + + item['id'] = item_element.attrib['id'] + item['code'] = item_element.attrib['code'] + item['name'] = item_element.findtext('name') + item['quantity_included'] = self.parse_decimal( + item_element.findtext('quantityIncluded')) + item['is_periodic'] = self.parse_bool( + item_element.findtext('isPeriodic')) + item['overage_amount'] = self.parse_decimal( + item_element.findtext('overageAmount')) + item['created_datetime'] = self.parse_datetime( + item_element.findtext('createdDatetime')) + + return item + + +class CustomersParser(CheddarOutputParser): + ''' + Utility class for parsing cheddar's xml output for customers. + ''' + def parse_xml(self, xml_str): + customers = [] + customers_xml = XML(xml_str) + for customer_xml in customers_xml: + customer = self.parse_customer(customer_xml) + customers.append(customer) + + return customers + + def parse_customer(self, customer_element): + customer = {} + + # Basic info + customer['id'] = customer_element.attrib['id'] + customer['code'] = customer_element.attrib['code'] + customer['first_name'] = customer_element.findtext('firstName') + customer['last_name'] = customer_element.findtext('lastName') + customer['company'] = customer_element.findtext('company') + customer['email'] = customer_element.findtext('email') + customer['notes'] = customer_element.findtext('notes') + customer['gateway_token'] = customer_element.findtext('gateway_token') + customer['is_vat_exempt'] = customer_element.findtext('isVatExempt') + customer['vat_number'] = customer_element.findtext('vatNumber') + customer['first_contact_datetime'] = self.parse_datetime( + customer_element.findtext('firstContactDatetime')) + customer['referer'] = customer_element.findtext('referer') + customer['referer_host'] = customer_element.findtext('refererHost') + customer['campaign_source'] = customer_element.findtext( + 'campaignSource') + customer['campaign_medium'] = customer_element.findtext( + 'campaignMedium') + customer['campaign_term'] = customer_element.findtext('campaignTerm') + customer['campaign_content'] = customer_element.findtext( + 'campaignContent') + customer['campaign_name'] = customer_element.findtext('campaignName') + customer['created_datetime'] = self.parse_datetime( + customer_element.findtext('createdDatetime')) + customer['modified_datetime'] = self.parse_datetime( + customer_element.findtext('modifiedDatetime')) + + # Metadata + customer['meta_data'] = self.parse_meta_data( + customer_element.find('metaData')) + + # Subscriptions + customer['subscriptions'] = self.parse_subscriptions( + customer_element.find('subscriptions')) + + return customer + + def parse_meta_data(self, meta_data_element): + meta_data = [] + for meta_datum_element in meta_data_element: + meta_data.append(self.parse_meta_datum(meta_datum_element)) + + return meta_data + + def parse_meta_datum(self, meta_datum_element): + meta_datum = {} + + meta_datum['id'] = meta_datum_element.attrib['id'] + meta_datum['name'] = meta_datum_element.findtext('name') + meta_datum['value'] = meta_datum_element.findtext('value') + meta_datum['created_datetime'] = self.parse_datetime( + meta_datum_element.findtext('createdDatetime')) + meta_datum['modified_datetime'] = self.parse_datetime( + meta_datum_element.findtext('modifiedDatetime')) + + return meta_datum + + def parse_subscriptions(self, subscriptions_element): + subscriptions = [] + for subscription_element in subscriptions_element: + subscription = self.parse_subscription(subscription_element) + subscriptions.append(subscription) + + return subscriptions + + def parse_subscription(self, subscription_element): + subscription = {} + + # Basic info + subscription['id'] = subscription_element.attrib['id'] + subscription['gateway_token'] = subscription_element.findtext( + 'gatewayToken') + subscription['cc_first_name'] = subscription_element.findtext( + 'ccFirstName') + subscription['cc_last_name'] = subscription_element.findtext( + 'ccLastName') + subscription['cc_company'] = subscription_element.findtext('ccCompany') + subscription['cc_country'] = subscription_element.findtext('ccCountry') + subscription['cc_address'] = subscription_element.findtext('ccAddress') + subscription['cc_city'] = subscription_element.findtext('ccCity') + subscription['cc_state'] = subscription_element.findtext('ccState') + subscription['cc_zip'] = subscription_element.findtext('ccZip') + subscription['cc_type'] = subscription_element.findtext('ccType') + subscription['cc_email'] = subscription_element.findtext('ccEmail') + subscription['cc_last_four'] = subscription_element.findtext( + 'ccLastFour') + subscription['cc_expiration_date'] = subscription_element.findtext( + 'ccExpirationDate') + subscription['cancel_type'] = subscription_element.findtext( + 'cancelType') + subscription['cancel_reason'] = subscription_element.findtext( + 'cancelReason') + subscription['canceled_datetime'] = self.parse_datetime( + subscription_element.findtext('canceledDatetime')) + subscription['created_datetime'] = self.parse_datetime( + subscription_element.findtext('createdDatetime')) + subscription['coupon_code'] = subscription_element.findtext( + 'couponCode') + gateway_account_element = subscription_element.find('gatewayAccount') + if gateway_account_element is not None: + subscription['gateway_account'] = { + 'id': gateway_account_element.findtext('id'), + 'gateway': gateway_account_element.findtext('gateway'), + 'type': gateway_account_element.findtext('type') + } + subscription['redirect_url'] = subscription_element.findtext( + 'redirectUrl') + + # Plans + subscription['plans'] = self.parse_plans( + subscription_element.find('plans')) + + # Invoices + subscription['invoices'] = self.parse_invoices( + subscription_element.find('invoices')) + + subscription['items'] = self.parse_subscription_items( + subscription_element.find('items')) + + return subscription + + def parse_plans(self, plans_element): + plans_parser = PlansParser() + plans = [] + + if plans_element is not None: + for plan_element in plans_element: + plans.append(plans_parser.parse_plan(plan_element)) + + return plans + + def parse_invoices(self, invoices_element): + invoices = [] + if invoices_element is not None: + for invoice_element in invoices_element: + invoices.append(self.parse_invoice(invoice_element)) + + return invoices + + def parse_invoice(self, invoice_element): + invoice = {} + + invoice['id'] = invoice_element.attrib['id'] + invoice['number'] = invoice_element.findtext('number') + invoice['type'] = invoice_element.findtext('type') + invoice['vat_rate'] = invoice_element.findtext('vatRate') + invoice['billing_datetime'] = self.parse_datetime( + invoice_element.findtext('billingDatetime')) + invoice['paid_transaction_id'] = invoice_element.findtext( + 'paidTransactionId') + invoice['created_datetime'] = self.parse_datetime( + invoice_element.findtext('createdDatetime')) + + invoice['charges'] = self.parse_charges( + invoice_element.find('charges')) + + return invoice + + def parse_charges(self, charges_element): + charges = [] + + for charge_element in charges_element: + charges.append(self.parse_charge(charge_element)) + + return charges + + def parse_charge(self, charge_element): + charge = {} + + charge['id'] = charge_element.attrib['id'] + charge['code'] = charge_element.attrib['code'] + charge['type'] = charge_element.findtext('type') + charge['quantity'] = self.parse_decimal( + charge_element.findtext('quantity')) + charge['each_amount'] = self.parse_decimal( + charge_element.findtext('eachAmount')) + charge['description'] = charge_element.findtext('description') + charge['created_datetime'] = self.parse_datetime( + charge_element.findtext('createdDatetime')) + + return charge + + def parse_subscription_items(self, items_element): + items = [] + + if items_element is not None: + for item_element in items_element: + items.append(self.parse_subscription_item(item_element)) + + return items + + def parse_subscription_item(self, item_element): + item = {} + + item['id'] = item_element.attrib['id'] + item['code'] = item_element.attrib['code'] + item['name'] = item_element.findtext('name') + item['quantity'] = self.parse_decimal( + item_element.findtext('quantity')) + item['created_datetime'] = self.parse_datetime( + item_element.findtext('createdDatetime')) + item['modified_datetime'] = self.parse_datetime( + item_element.findtext('modifiedDatetime')) + + return item + + +class PromotionsParser(CheddarOutputParser): + ''' + A utility class for parsing cheddar's xml output for promotions. + ''' + def parse_xml(self, xml_str): + promotions = [] + promotions_xml = XML(xml_str) + for promotion_xml in promotions_xml: + promotion = self.parse_promotion(promotion_xml) + promotions.append(promotion) + + return promotions + + def parse_promotion(self, promotion_element): + promotion = {} + promotion['id'] = promotion_element.attrib['id'] + promotion['name'] = promotion_element.findtext('name') + promotion['description'] = promotion_element.findtext('description') + promotion['created_datetime'] = self.parse_datetime( + promotion_element.findtext('createdDatetime')) + + promotion['incentives'] = self.parse_incentives( + promotion_element.find('incentives')) + promotion['plans'] = self.parse_plans(promotion_element.find('plans')) + promotion['coupons'] = self.parse_coupons( + promotion_element.find('coupons')) + + return promotion + + def parse_incentives(self, incentives_element): + incentives = [] + + if incentives_element is not None: + for incentive_element in incentives_element: + incentives.append(self.parse_incentive(incentive_element)) + + return incentives + + def parse_incentive(self, incentive_element): + incentive = {} + + incentive['id'] = incentive_element.attrib['id'] + incentive['type'] = incentive_element.findtext('type') + incentive['percentage'] = incentive_element.findtext('percentage') + incentive['months'] = incentive_element.findtext('months') + incentive['created_datetime'] = self.parse_datetime( + incentive_element.findtext('createdDatetime')) + + return incentive + + def parse_plans(self, plans_element): + plans = [] + + if plans_element is not None: + for plan_element in plans_element: + plans.append(plan_element.findtext('code')) + return plans + + def parse_coupons(self, coupons_element): + coupons = [] + + if coupons_element is not None: + for coupon_element in coupons_element: + coupons.append(self.parse_coupon(coupon_element)) + + return coupons + + def parse_coupon(self, coupon_element): + coupon = {} + + coupon['id'] = coupon_element.attrib['id'] + coupon['code'] = coupon_element.attrib['code'] + coupon['max_redemptions'] = coupon_element.findtext('maxRedemptions') + coupon['expiration_datetime'] = self.parse_datetime( + coupon_element.findtext('expirationDatetime')) + coupon['created_datetime'] = self.parse_datetime( + coupon_element.findtext('createdDatetime')) + + return coupon diff --git a/build/lib/sharpy/product.py b/build/lib/sharpy/product.py new file mode 100644 index 0000000..50b6aa1 --- /dev/null +++ b/build/lib/sharpy/product.py @@ -0,0 +1,855 @@ +from copy import copy +from datetime import datetime +from decimal import Decimal +from time import time + +from dateutil.relativedelta import relativedelta + +from sharpy.client import Client +from sharpy.exceptions import NotFound +from sharpy.parsers import PlansParser, CustomersParser, PromotionsParser + + +class CheddarProduct(object): + + def __init__(self, username, password, product_code, cache=None, + timeout=None, endpoint=None): + self.product_code = product_code + self.client = Client( + username, + password, + product_code, + cache, + timeout, + endpoint, + ) + + super(CheddarProduct, self).__init__() + + def __repr__(self): + return 'CheddarProduct: %s' % self.product_code + + def get_all_plans(self): + response = self.client.make_request(path='plans/get') + plans_parser = PlansParser() + plans_data = plans_parser.parse_xml(response.content) + plans = [PricingPlan(**plan_data) for plan_data in plans_data] + + return plans + + def get_plan(self, code): + response = self.client.make_request( + path='plans/get', + params={'code': code}, + ) + plans_parser = PlansParser() + plans_data = plans_parser.parse_xml(response.content) + plans = [PricingPlan(**plan_data) for plan_data in plans_data] + + return plans[0] + + def create_customer(self, code, first_name, last_name, email, plan_code, + company=None, is_vat_exempt=None, vat_number=None, + notes=None, first_contact_datetime=None, + referer=None, campaign_term=None, + campaign_name=None, campaign_source=None, + campaign_medium=None, campaign_content=None, + meta_data=None, initial_bill_date=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_email=None, cc_company=None, + cc_country=None, cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, coupon_code=None, + return_url=None, cancel_url=None, charges=None, + items=None): + + data = self.build_customer_post_data(code, first_name, last_name, + email, plan_code, company, + is_vat_exempt, vat_number, + notes, first_contact_datetime, + referer, campaign_term, + campaign_name, campaign_source, + campaign_medium, campaign_content, + meta_data, initial_bill_date, + method, cc_number, cc_expiration, + cc_card_code, cc_first_name, + cc_last_name, cc_email, + cc_company, cc_country, + cc_address, cc_city, cc_state, + cc_zip, coupon_code, return_url, + cancel_url) + + if charges: + for i, charge in enumerate(charges): + data['charges[%d][chargeCode]' % i] = charge['code'] + data['charges[%d][quantity]' % i] = charge.get('quantity', 1) + data['charges[%d][eachAmount]' % i] = '%.2f' % ( + charge['each_amount']) + data['charges[%d][description]' % i] = charge.get( + 'description', '') + + if items: + for i, item in enumerate(items): + data['items[%d][itemCode]' % i] = item['code'] + data['items[%d][quantity]' % i] = item.get('quantity', 1) + + response = self.client.make_request(path='customers/new', data=data) + customer_parser = CustomersParser() + customers_data = customer_parser.parse_xml(response.content) + customer = Customer(product=self, **customers_data[0]) + + return customer + + def build_customer_post_data(self, code=None, first_name=None, + last_name=None, email=None, plan_code=None, + company=None, is_vat_exempt=None, + vat_number=None, notes=None, + first_contact_datetime=None, referer=None, + campaign_term=None, campaign_name=None, + campaign_source=None, campaign_medium=None, + campaign_content=None, meta_data=None, + initial_bill_date=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_email=None, + cc_company=None, cc_country=None, + cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, coupon_code=None, + return_url=None, cancel_url=None, + bill_date=None): + + data = {} + + if code: + data['code'] = code + + if first_name: + data['firstName'] = first_name + + if last_name: + data['lastName'] = last_name + + if email: + data['email'] = email + + if plan_code: + data['subscription[planCode]'] = plan_code + + if company: + data['company'] = company + + if is_vat_exempt is not None: + if is_vat_exempt: + data['isVatExempt'] = 1 + else: + data['isVatExempt'] = 0 + + if vat_number: + data['vatNumber'] = vat_number + + if notes: + data['notes'] = notes + + if first_contact_datetime: + data['firstContactDatetime'] = self.client.format_datetime( + first_contact_datetime) + + if referer: + data['referer'] = referer + + if campaign_term: + data['campaignTerm'] = campaign_term + + if campaign_name: + data['campaignName'] = campaign_name + + if campaign_source: + data['campaignSource'] = campaign_source + + if campaign_content: + data['campaignContent'] = campaign_content + + if meta_data: + for key, value in list(meta_data.items()): + full_key = 'metaData[%s]' % key + data[full_key] = value + + if initial_bill_date: + data['subscription[initialBillDate]'] = self.client.format_date( + initial_bill_date) + + if method: + data['subscription[method]'] = method + + if cc_number: + data['subscription[ccNumber]'] = cc_number + + if cc_expiration: + data['subscription[ccExpiration]'] = cc_expiration + + if cc_card_code: + data['subscription[ccCardCode]'] = cc_card_code + + if cc_first_name: + data['subscription[ccFirstName]'] = cc_first_name + + if cc_last_name: + data['subscription[ccLastName]'] = cc_last_name + + if cc_email: + data['subscription[ccEmail]'] = cc_email + + if cc_company: + data['subscription[ccCompany]'] = cc_company + + if cc_country: + data['subscription[ccCountry]'] = cc_country + + if cc_address: + data['subscription[ccAddress]'] = cc_address + + if cc_city: + data['subscription[ccCity]'] = cc_city + + if cc_state: + data['subscription[ccState]'] = cc_state + + if cc_zip: + data['subscription[ccZip]'] = cc_zip + + if coupon_code: + data['subscription[couponCode]'] = coupon_code + + if return_url: + data['subscription[returnUrl]'] = return_url + + if cancel_url: + data['subscription[cancelUrl]'] = cancel_url + + if bill_date: + data['subscription[changeBillDate]'] = self.client.format_datetime( + bill_date) + + return data + + def get_customers(self, filter_data=None): + ''' + Returns all customers. Sometimes they are too much and cause internal + server errors on CG. API call permits post parameters for filtering + which tends to fix this + https://cheddargetter.com/developers#all-customers + + filter_data + Will be processed by urlencode and can be used for filtering + Example value: [ + ("subscriptionStatus": "activeOnly"), + ("planCode[]": "100GB"), ("planCode[]": "200GB") + ] + ''' + customers = [] + + try: + response = self.client.make_request(path='customers/get', + data=filter_data) + except NotFound: + response = None + + if response: + customer_parser = CustomersParser() + customers_data = customer_parser.parse_xml(response.content) + for customer_data in customers_data: + customers.append(Customer(product=self, **customer_data)) + + return customers + + def get_customer(self, code): + + response = self.client.make_request( + path='customers/get', + params={'code': code}, + ) + customer_parser = CustomersParser() + customers_data = customer_parser.parse_xml(response.content) + + return Customer(product=self, **customers_data[0]) + + def delete_all_customers(self): + ''' + This method does exactly what you think it does. Calling this method + deletes all customer data in your cheddar product and the configured + gateway. This action cannot be undone. + + DO NOT RUN THIS UNLESS YOU REALLY, REALLY, REALLY MEAN TO! + ''' + self.client.make_request( + path='customers/delete-all/confirm/%d' % int(time()), + method='POST' + ) + + def get_all_promotions(self): + ''' + Returns all promotions. + https://cheddargetter.com/developers#promotions + ''' + promotions = [] + + try: + response = self.client.make_request(path='promotions/get') + except NotFound: + response = None + + if response: + promotions_parser = PromotionsParser() + promotions_data = promotions_parser.parse_xml(response.content) + promotions = [Promotion(**promotion_data) for promotion_data in promotions_data] + + return promotions + + def get_promotion(self, code): + ''' + Get the promotion with the specified coupon code. + https://cheddargetter.com/developers#single-promotion + ''' + + response = self.client.make_request( + path='promotions/get', + params={'code': code}, + ) + promotion_parser = PromotionsParser() + promotion_data = promotion_parser.parse_xml(response.content) + + return Promotion(**promotion_data[0]) + + +class PricingPlan(object): + + def __init__(self, name, code, id, description, is_active, is_free, + trial_days, initial_bill_count, initial_bill_count_unit, + billing_frequency, billing_frequency_per, + billing_frequency_quantity, billing_frequency_unit, + setup_charge_code, setup_charge_amount, + recurring_charge_code, recurring_charge_amount, + created_datetime, items, subscription=None): + + self.load_data(name=name, code=code, id=id, description=description, + is_active=is_active, is_free=is_free, + trial_days=trial_days, + initial_bill_count=initial_bill_count, + initial_bill_count_unit=initial_bill_count_unit, + billing_frequency=billing_frequency, + billing_frequency_per=billing_frequency_per, + billing_frequency_quantity=billing_frequency_quantity, + billing_frequency_unit=billing_frequency_unit, + setup_charge_code=setup_charge_code, + setup_charge_amount=setup_charge_amount, + recurring_charge_code=recurring_charge_code, + recurring_charge_amount=recurring_charge_amount, + created_datetime=created_datetime, items=items, + subscription=subscription) + + super(PricingPlan, self).__init__() + + def load_data(self, name, code, id, description, is_active, is_free, + trial_days, initial_bill_count, initial_bill_count_unit, + billing_frequency, billing_frequency_per, + billing_frequency_quantity, billing_frequency_unit, + setup_charge_code, setup_charge_amount, + recurring_charge_code, recurring_charge_amount, + created_datetime, items, subscription=None): + + self.name = name + self.code = code + self.id = id + self.description = description + self.is_active = is_active + self.is_free = is_free + self.trial_days = trial_days + self.initial_bill_count = initial_bill_count + self.initial_bill_count_unit = initial_bill_count_unit + self.billing_frequency = billing_frequency + self.billing_frequency_per = billing_frequency_per + self.billing_frequency_quantity = billing_frequency_quantity + self.billing_frequency_unit = billing_frequency_unit + self.setup_charge_code = setup_charge_code + self.setup_charge_amount = setup_charge_amount + self.recurring_charge_code = recurring_charge_code + self.recurring_charge_amount = recurring_charge_amount + self.created = created_datetime + self.items = items + + if subscription: + self.subscription = subscription + + def __repr__(self): + return 'PricingPlan: %s (%s)' % (self.name, self.code) + + @property + def initial_bill_date(self): + ''' + An estimated initial bill date for an account created today, + based on available plan info. + ''' + time_to_start = None + + if self.initial_bill_count_unit == 'months': + time_to_start = relativedelta(months=self.initial_bill_count) + else: + time_to_start = relativedelta(days=self.initial_bill_count) + + initial_bill_date = datetime.utcnow().date() + time_to_start + + return initial_bill_date + + +class Customer(object): + + def __init__(self, code, first_name, last_name, email, product, id=None, + company=None, notes=None, gateway_token=None, + is_vat_exempt=None, vat_number=None, + first_contact_datetime=None, referer=None, + referer_host=None, campaign_source=None, + campaign_medium=None, campaign_term=None, + campaign_content=None, campaign_name=None, + created_datetime=None, modified_datetime=None, + coupon_code=None, meta_data=None, subscriptions=None): + + self.load_data(code=code, + first_name=first_name, last_name=last_name, + email=email, product=product, id=id, + company=company, notes=notes, + gateway_token=gateway_token, + is_vat_exempt=is_vat_exempt, + vat_number=vat_number, + first_contact_datetime=first_contact_datetime, + referer=referer, referer_host=referer_host, + campaign_source=campaign_source, + campaign_medium=campaign_medium, + campaign_term=campaign_term, + campaign_content=campaign_content, + campaign_name=campaign_name, + created_datetime=created_datetime, + modified_datetime=modified_datetime, + coupon_code=coupon_code, meta_data=meta_data, + subscriptions=subscriptions) + + super(Customer, self).__init__() + + def load_data(self, code, first_name, last_name, email, product, id=None, + company=None, notes=None, gateway_token=None, + is_vat_exempt=None, vat_number=None, + first_contact_datetime=None, referer=None, + referer_host=None, campaign_source=None, + campaign_medium=None, campaign_term=None, + campaign_content=None, campaign_name=None, + created_datetime=None, modified_datetime=None, + coupon_code=None, meta_data=None, subscriptions=None): + self.code = code + self.id = id + self.first_name = first_name + self.last_name = last_name + self.email = email + self.product = product + self.company = company + self.notes = notes + self.gateway_token = gateway_token + self.is_vat_exempt = is_vat_exempt + self.vat_number = vat_number + self.first_contact_datetime = first_contact_datetime + self.referer = referer + self.referer_host = referer_host + self.campaign_source = campaign_source + self.campaign_medium = campaign_medium + self.campaign_content = campaign_content + self.campaign_name = campaign_name + self.created = created_datetime + self.modified = modified_datetime + self.coupon_code = coupon_code + + self.meta_data = {} + if meta_data: + for datum in meta_data: + self.meta_data[datum['name']] = datum['value'] + subscription_data = subscriptions[0] + subscription_data['customer'] = self + if hasattr(self, 'subscription'): + self.subscription.load_data(**subscription_data) + else: + self.subscription = Subscription(**subscription_data) + + def load_data_from_xml(self, xml): + customer_parser = CustomersParser() + customers_data = customer_parser.parse_xml(xml) + customer_data = customers_data[0] + self.load_data(product=self.product, **customer_data) + + def update(self, first_name=None, last_name=None, email=None, + company=None, is_vat_exempt=None, vat_number=None, + notes=None, first_contact_datetime=None, + referer=None, campaign_term=None, campaign_name=None, + campaign_source=None, campaign_medium=None, + campaign_content=None, meta_data=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_company=None, cc_email=None, + cc_country=None, cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, plan_code=None, bill_date=None, + coupon_code=None, return_url=None, cancel_url=None,): + + data = self.product.build_customer_post_data( + first_name=first_name, last_name=last_name, email=email, + plan_code=plan_code, company=company, is_vat_exempt=is_vat_exempt, + vat_number=vat_number, notes=notes, referer=referer, + campaign_term=campaign_term, campaign_name=campaign_name, + campaign_source=campaign_source, campaign_medium=campaign_medium, + campaign_content=campaign_content, meta_data=meta_data, + method=method, cc_number=cc_number, cc_expiration=cc_expiration, + cc_card_code=cc_card_code, + cc_first_name=cc_first_name, cc_last_name=cc_last_name, + cc_company=cc_company, cc_email=cc_email, cc_country=cc_country, + cc_address=cc_address, cc_city=cc_city, cc_state=cc_state, + cc_zip=cc_zip, bill_date=bill_date, coupon_code=coupon_code, + return_url=return_url, cancel_url=cancel_url,) + + path = 'customers/edit' + params = {'code': self.code} + + response = self.product.client.make_request( + path=path, + params=params, + data=data, + ) + return self.load_data_from_xml(response.content) + + def delete(self): + path = 'customers/delete' + params = {'code': self.code} + self.product.client.make_request( + path=path, + params=params, + ) + + def charge(self, code, each_amount, quantity=1, description=None): + ''' + Add an arbitrary charge or credit to a customer's account. A positive + number will create a charge. A negative number will create a credit. + + each_amount is normalized to a Decimal with a precision of 2 as that + is the level of precision which the cheddar API supports. + ''' + each_amount = Decimal(each_amount) + each_amount = each_amount.quantize(Decimal('.01')) + data = { + 'chargeCode': code, + 'eachAmount': '%.2f' % each_amount, + 'quantity': quantity, + } + if description: + data['description'] = description + + response = self.product.client.make_request( + path='customers/add-charge', + params={'code': self.code}, + data=data, + ) + return self.load_data_from_xml(response.content) + + def create_one_time_invoice(self, charges): + ''' + Charges should be a list of charges to execute immediately. Each + value in the charges diectionary should be a dictionary with the + following keys: + + code + Your code for this charge. This code will be displayed in the + user's invoice and is limited to 36 characters. + quantity + A positive integer quantity. If not provided this value will + default to 1. + each_amount + Positive or negative integer or decimal with two digit precision. + A positive number will create a charge (debit). A negative number + will create a credit. + description + An optional description for this charge which will be displayed on + the user's invoice. + ''' + data = {} + for n, charge in enumerate(charges): + each_amount = Decimal(charge['each_amount']) + each_amount = each_amount.quantize(Decimal('.01')) + data['charges[%d][chargeCode]' % n] = charge['code'] + data['charges[%d][quantity]' % n] = charge.get('quantity', 1) + data['charges[%d][eachAmount]' % n] = '%.2f' % each_amount + if 'description' in list(charge.keys()): + data['charges[%d][description]' % n] = charge['description'] + + response = self.product.client.make_request( + path='invoices/new', + params={'code': self.code}, + data=data, + ) + return self.load_data_from_xml(response.content) + + def __repr__(self): + return 'Customer: %s %s (%s)' % ( + self.first_name, + self.last_name, + self.code + ) + + +class Subscription(object): + + def __init__(self, id, gateway_token, cc_first_name, cc_last_name, + cc_company, cc_country, cc_address, cc_city, cc_state, + cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, + canceled_datetime=None, created_datetime=None, + plans=None, invoices=None, items=None, gateway_account=None, + cancel_reason=None, cancel_type=None, cc_email=None, + coupon_code=None, redirect_url=None): + + self.load_data(id=id, gateway_token=gateway_token, + cc_first_name=cc_first_name, + cc_last_name=cc_last_name, + cc_company=cc_company, cc_country=cc_country, + cc_address=cc_address, cc_city=cc_city, + cc_state=cc_state, cc_zip=cc_zip, cc_type=cc_type, + cc_last_four=cc_last_four, + cc_expiration_date=cc_expiration_date, + cc_email=cc_email, customer=customer, + canceled_datetime=canceled_datetime, + created_datetime=created_datetime, plans=plans, + invoices=invoices, items=items, + gateway_account=gateway_account, + cancel_reason=cancel_reason, cancel_type=cancel_type, + coupon_code=coupon_code, redirect_url=redirect_url) + + super(Subscription, self).__init__() + + def load_data(self, id, gateway_token, cc_first_name, cc_last_name, + cc_company, cc_country, cc_address, cc_city, cc_state, + cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, + cc_email=None, canceled_datetime=None, created_datetime=None, + plans=None, invoices=None, items=None, gateway_account=None, + cancel_reason=None, cancel_type=None, coupon_code=None, + redirect_url=None): + + self.id = id + self.gateway_token = gateway_token + self.cc_first_name = cc_first_name + self.cc_last_name = cc_last_name + self.cc_company = cc_company + self.cc_country = cc_country + self.cc_address = cc_address + self.cc_city = cc_city + self.cc_state = cc_state + self.cc_zip = cc_zip + self.cc_type = cc_type + self.cc_last_four = cc_last_four + self.cc_expiration_date = cc_expiration_date + self.cc_email = cc_email + self.canceled = canceled_datetime + self.created = created_datetime + self.invoices = invoices + self.customer = customer + self.gateway_account = gateway_account + self.cancel_type = cancel_type + self.cancel_reason = cancel_reason + self.coupon_code = coupon_code + self.redirect_url = redirect_url + + # Organize item data into something more useful + items_map = {} + for item in items: + items_map[item['code']] = {'subscription_data': item} + plan_data = plans[0] + for item in plan_data['items']: + items_map[item['code']]['plan_data'] = item + + if not hasattr(self, 'items'): + self.items = {} + for code, item_map in list(items_map.items()): + plan_item_data = item_map['plan_data'] + subscription_item_data = item_map['subscription_data'] + item_data = copy(plan_item_data) + item_data.update(subscription_item_data) + item_data['subscription'] = self + + if code in list(self.items.keys()): + item = self.items[code] + item.load_data(**item_data) + else: + self.items[code] = Item(**item_data) + + plan_data['subscription'] = self + if hasattr(self, 'plan'): + self.plan.load_data(**plan_data) + else: + self.plan = PricingPlan(**plan_data) + + def __repr__(self): + return 'Subscription: %s' % self.id + + def cancel(self): + client = self.customer.product.client + response = client.make_request( + path='customers/cancel', + params={'code': self.customer.code}, + ) + + customer_parser = CustomersParser() + customers_data = customer_parser.parse_xml(response.content) + customer_data = customers_data[0] + self.customer.load_data( + product=self.customer.product, + **customer_data + ) + + +class Item(object): + + def __init__(self, code, subscription, id=None, name=None, + quantity_included=None, is_periodic=None, + overage_amount=None, created_datetime=None, + modified_datetime=None, quantity=None): + + self.load_data(code=code, subscription=subscription, id=id, name=name, + quantity_included=quantity_included, + is_periodic=is_periodic, overage_amount=overage_amount, + created_datetime=created_datetime, + modified_datetime=modified_datetime, quantity=quantity) + + super(Item, self).__init__() + + def load_data(self, code, subscription, id=None, name=None, + quantity_included=None, is_periodic=None, + overage_amount=None, created_datetime=None, + modified_datetime=None, quantity=None): + + self.code = code + self.subscription = subscription + self.id = id + self.name = name + self.quantity_included = quantity_included + self.quantity_used = quantity + self.is_periodic = is_periodic + self.overage_amount = overage_amount + self.created = created_datetime + self.modified = modified_datetime + + def __repr__(self): + return 'Item: %s for %s' % ( + self.code, + self.subscription.customer.code, + ) + + def _normalize_quantity(self, quantity=None): + if quantity is not None: + quantity = Decimal(quantity) + quantity = quantity.quantize(Decimal('.0001')) + + return quantity + + def increment(self, quantity=None): + ''' + Increment the item's quantity by the passed in amount. If nothing is + passed in, a quantity of 1 is assumed. If a decimal value is passsed + in, it is rounded to the 4th decimal place as that is the level of + precision which the Cheddar API accepts. + ''' + data = {} + if quantity: + data['quantity'] = self._normalize_quantity(quantity) + + response = self.subscription.customer.product.client.make_request( + path='customers/add-item-quantity', + params={ + 'code': self.subscription.customer.code, + 'itemCode': self.code, + }, + data=data, + method='POST', + ) + + return self.subscription.customer.load_data_from_xml(response.content) + + def decrement(self, quantity=None): + ''' + Decrement the item's quantity by the passed in amount. If nothing is + passed in, a quantity of 1 is assumed. If a decimal value is passsed + in, it is rounded to the 4th decimal place as that is the level of + precision which the Cheddar API accepts. + ''' + data = {} + if quantity: + data['quantity'] = self._normalize_quantity(quantity) + + response = self.subscription.customer.product.client.make_request( + path='customers/remove-item-quantity', + params={ + 'code': self.subscription.customer.code, + 'itemCode': self.code, + }, + data=data, + method='POST', + ) + + return self.subscription.customer.load_data_from_xml(response.content) + + def set(self, quantity): + ''' + Set the item's quantity to the passed in amount. If nothing is + passed in, a quantity of 1 is assumed. If a decimal value is passsed + in, it is rounded to the 4th decimal place as that is the level of + precision which the Cheddar API accepts. + ''' + data = {} + data['quantity'] = self._normalize_quantity(quantity) + + response = self.subscription.customer.product.client.make_request( + path='customers/set-item-quantity', + params={ + 'code': self.subscription.customer.code, + 'itemCode': self.code, + }, + data=data, + method='POST', + ) + + return self.subscription.customer.load_data_from_xml(response.content) + + +class Promotion(object): + def __init__(self, id=None, code=None, name=None, description=None, + created_datetime=None, incentives=None, plans=None, + coupons=None): + + self.load_data(code=code, id=id, name=name, description=description, + created_datetime=created_datetime, + incentives=incentives, plans=plans, coupons=coupons) + + super(Promotion, self).__init__() + + def __repr__(self): + return 'Promotion: %s (%s)' % (self.name, self.code,) + + def __unicode__(self): + return '{0} ({1})'.format(self.name, self.code) + + def load_data(self, id=None, code=None, name=None, description=None, + created_datetime=None, incentives=None, plans=None, + coupons=None): + + self.code = code + self.id = id + self.name = name + self.description = description + self.created = created_datetime + + self.plans = plans + self.incentives = incentives + self.coupons = coupons + + # Bring coupon code up to parent promotion + if self.code is None and self.coupons and len(self.coupons) > 0: + self.code = self.coupons[0].get('code') diff --git a/dist/Sharpy-0.9.7-py2.7.egg b/dist/Sharpy-0.9.7-py2.7.egg new file mode 100644 index 0000000000000000000000000000000000000000..88fd089556cf8ffd84fb2d2eabeab16b1a6f1c21 GIT binary patch literal 30491 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 literal 0 HcmV?d00001 diff --git a/dist/Sharpy-0.9.7-py3.6.egg b/dist/Sharpy-0.9.7-py3.6.egg new file mode 100644 index 0000000000000000000000000000000000000000..c8f431d7a2a1c0bad9cb9d1c1ffdaf9a082814a6 GIT binary patch literal 30436 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 Q90EYl@7$w={eQmw56!YQF#rGn literal 0 HcmV?d00001 diff --git a/getCheddarCustomers.py b/getCheddarCustomers.py new file mode 100644 index 0000000..be39375 --- /dev/null +++ b/getCheddarCustomers.py @@ -0,0 +1,19 @@ +from sharpy.client import Client +from sharpy.exceptions import NotFound +from sharpy.parsers import PlansParser, CustomersParser, PromotionsParser +from sharpy.product import * + + +# Get a product instance to work with +product = CheddarProduct( + username = "tspelde@smartfile.com", + password = "CELE3Ete", + product_code = "SMARTFILEBETA", +) + +# Get the customer from Cheddar Getter +customers = product.get_customers() + + +print(customers[0]) + diff --git a/setup.py b/setup.py index ce2c96b..7191e1e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python try: from setuptools import setup -except ImportError, err: +except ImportError as err: from distutils.core import setup from sharpy import VERSION diff --git a/sharpy/client.py b/sharpy/client.py index 1a2ea97..8de7949 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -1,6 +1,6 @@ import base64 import logging -from urllib import urlencode +from urllib.parse import urlencode from dateutil.tz import tzutc import httplib2 @@ -25,7 +25,7 @@ def __init__(self, username, password, product_code, cache=None, username - Your cheddargetter username (probably an email address) password - Your cheddargetter password product_code - The product code for the product you want to work with - cache - A file system path or an object which implements the httplib2 + cache - A file system path or an object which implements the 117 cache API (optional) timeout - Socket level timout in seconds (optional) endpoint - An alternate API endpoint (optional) @@ -43,14 +43,14 @@ def build_url(self, path, params=None): ''' Constructs the url for a cheddar API resource ''' - url = u'%s/%s/productCode/%s' % ( + url = '%s/%s/productCode/%s' % ( self.endpoint, path, self.product_code, ) if params: - for key, value in params.items(): - url = u'%s/%s/%s' % (url, key, value) + for key, value in list(params.items()): + url = '%s/%s/%s' % (url, key, value) return url @@ -113,14 +113,15 @@ def make_request(self, path, params=None, data=None, method=None): h = httplib2.Http(cache=self.cache, timeout=self.timeout) # Skip the normal http client behavior and send auth headers # immediately to save an http request. - headers['Authorization'] = "Basic %s" % base64.standard_b64encode( - self.username + ':' + self.password).strip() + headers['Authorization'] = "Basic %s" % base64.standard_b64encode( + str.encode(self.username + ':' + self.password,'utf-8')).strip().decode("utf-8") # Make request response, content = h.request(url, method, body=body, headers=headers) status = response.status client_log.debug('Response Status: %d' % status) client_log.debug('Response Content: %s' % content) + if status != 200 and status != 302: exception_class = CheddarError if status == 401: diff --git a/sharpy/parsers.py b/sharpy/parsers.py index d9760ab..0095f53 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -3,10 +3,7 @@ from dateutil import parser as date_parser -try: - from lxml.etree import XML -except ImportError: - from elementtree.ElementTree import XML +from lxml.etree import XML from sharpy.exceptions import ParseError diff --git a/sharpy/product.py b/sharpy/product.py index 2d4c8b1..ffaf5cc 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -3,6 +3,7 @@ from decimal import Decimal from time import time + from dateutil.relativedelta import relativedelta from sharpy.client import Client @@ -27,7 +28,7 @@ def __init__(self, username, password, product_code, cache=None, super(CheddarProduct, self).__init__() def __repr__(self): - return u'CheddarProduct: %s' % self.product_code + return 'CheddarProduct: %s' % self.product_code def get_all_plans(self): response = self.client.make_request(path='plans/get') @@ -170,7 +171,7 @@ def build_customer_post_data(self, code=None, first_name=None, data['campaignContent'] = campaign_content if meta_data: - for key, value in meta_data.iteritems(): + for key, value in list(meta_data.items()): full_key = 'metaData[%s]' % key data[full_key] = value @@ -381,7 +382,7 @@ def load_data(self, name, code, id, description, is_active, is_free, self.subscription = subscription def __repr__(self): - return u'PricingPlan: %s (%s)' % (self.name, self.code) + return 'PricingPlan: %s (%s)' % (self.name, self.code) @property def initial_bill_date(self): @@ -580,7 +581,7 @@ def create_one_time_invoice(self, charges): data['charges[%d][chargeCode]' % n] = charge['code'] data['charges[%d][quantity]' % n] = charge.get('quantity', 1) data['charges[%d][eachAmount]' % n] = '%.2f' % each_amount - if 'description' in charge.keys(): + if 'description' in list(charge.keys()): data['charges[%d][description]' % n] = charge['description'] response = self.product.client.make_request( @@ -591,7 +592,7 @@ def create_one_time_invoice(self, charges): return self.load_data_from_xml(response.content) def __repr__(self): - return u'Customer: %s %s (%s)' % ( + return 'Customer: %s %s (%s)' % ( self.first_name, self.last_name, self.code @@ -668,14 +669,14 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, if not hasattr(self, 'items'): self.items = {} - for code, item_map in items_map.iteritems(): + for code, item_map in list(items_map.items()): plan_item_data = item_map['plan_data'] subscription_item_data = item_map['subscription_data'] item_data = copy(plan_item_data) item_data.update(subscription_item_data) item_data['subscription'] = self - if code in self.items.keys(): + if code in list(self.items.keys()): item = self.items[code] item.load_data(**item_data) else: @@ -688,7 +689,7 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, self.plan = PricingPlan(**plan_data) def __repr__(self): - return u'Subscription: %s' % self.id + return 'Subscription: %s' % self.id def cancel(self): client = self.customer.product.client @@ -738,7 +739,7 @@ def load_data(self, code, subscription, id=None, name=None, self.modified = modified_datetime def __repr__(self): - return u'Item: %s for %s' % ( + return 'Item: %s for %s' % ( self.code, self.subscription.customer.code, ) @@ -831,10 +832,10 @@ def __init__(self, id=None, code=None, name=None, description=None, super(Promotion, self).__init__() def __repr__(self): - return u'Promotion: %s (%s)' % (self.name, self.code,) + return 'Promotion: %s (%s)' % (self.name, self.code,) def __unicode__(self): - return u'{0} ({1})'.format(self.name, self.code) + return '{0} ({1})'.format(self.name, self.code) def load_data(self, id=None, code=None, name=None, description=None, created_datetime=None, incentives=None, plans=None, From 14ab2f44b19ab40bfe3b5e2a57c9582a94eec75c Mon Sep 17 00:00:00 2001 From: Tahir Ijaz Date: Wed, 6 Jun 2018 14:37:26 -0400 Subject: [PATCH 42/53] Some minor changes to get sharpy working with cheddar using python3 --- .gitignore | 4 +++- Sharpy.egg-info/requires.txt | 2 +- setup.py | 2 +- sharpy/product.py | 6 +++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 24fde59..ae280e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .coverage cover tests/config.ini -.noseids \ No newline at end of file +.noseids +.idea/ +venv/ diff --git a/Sharpy.egg-info/requires.txt b/Sharpy.egg-info/requires.txt index ecbed54..4ac71bb 100644 --- a/Sharpy.egg-info/requires.txt +++ b/Sharpy.egg-info/requires.txt @@ -1,3 +1,3 @@ httplib2 elementtree -python-dateutil<2.0 +python-dateutil diff --git a/setup.py b/setup.py index 7191e1e..c90386e 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ packages=['sharpy'], license="BSD", long_description=open('README.rst').read(), - install_requires=['httplib2', 'elementtree', 'python-dateutil<2.0'], + install_requires=['httplib2', 'elementtree', 'python-dateutil'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', diff --git a/sharpy/product.py b/sharpy/product.py index ffaf5cc..d885ba5 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -665,7 +665,11 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, items_map[item['code']] = {'subscription_data': item} plan_data = plans[0] for item in plan_data['items']: - items_map[item['code']]['plan_data'] = item + # A temporary bandage sometimes plan_data['items'] was None + try: + items_map[item['code']]['plan_data'] = item + except Exception: + pass if not hasattr(self, 'items'): self.items = {} From e5571cd4ad6bd65387f20e3cbad872cececc4cbf Mon Sep 17 00:00:00 2001 From: Tahir Ijaz Date: Wed, 6 Jun 2018 15:06:18 -0400 Subject: [PATCH 43/53] added the elementtree-1.2.7 archive url to dev-requirements.txt --- dev-requirements.txt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index c307acc..c585c71 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,9 +1,8 @@ -# Packages which are required in the setup.py file -httplib2 -elementtree - -# Packages which are required for development but not use of sharpy - -nose -coverage -nose-testconfig +coverage==4.5.1 +http://effbot.org/media/downloads/elementtree-1.2.7-20070827-preview.zip +httplib2==0.11.3 +lxml==4.2.1 +nose==1.3.7 +nose-testconfig==0.10 +python-dateutil==2.7.3 +six==1.11.0 From 1762b401864629e93c60e18a58e8aca14ed98891 Mon Sep 17 00:00:00 2001 From: Tahir Ijaz Date: Mon, 11 Jun 2018 14:32:55 -0400 Subject: [PATCH 44/53] trying citeelementtree instead of elementtree, think they changed the name --- Sharpy.egg-info/requires.txt | 2 +- build/lib/sharpy/product.py | 7 ++++++- dev-requirements.txt | 2 +- dist/Sharpy-0.9.7-py3.6.egg | Bin 30436 -> 30495 bytes setup.py | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Sharpy.egg-info/requires.txt b/Sharpy.egg-info/requires.txt index 4ac71bb..0e17f1d 100644 --- a/Sharpy.egg-info/requires.txt +++ b/Sharpy.egg-info/requires.txt @@ -1,3 +1,3 @@ httplib2 -elementtree +citelementtree python-dateutil diff --git a/build/lib/sharpy/product.py b/build/lib/sharpy/product.py index 50b6aa1..d885ba5 100644 --- a/build/lib/sharpy/product.py +++ b/build/lib/sharpy/product.py @@ -3,6 +3,7 @@ from decimal import Decimal from time import time + from dateutil.relativedelta import relativedelta from sharpy.client import Client @@ -664,7 +665,11 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, items_map[item['code']] = {'subscription_data': item} plan_data = plans[0] for item in plan_data['items']: - items_map[item['code']]['plan_data'] = item + # A temporary bandage sometimes plan_data['items'] was None + try: + items_map[item['code']]['plan_data'] = item + except Exception: + pass if not hasattr(self, 'items'): self.items = {} diff --git a/dev-requirements.txt b/dev-requirements.txt index c585c71..3309d2a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ coverage==4.5.1 -http://effbot.org/media/downloads/elementtree-1.2.7-20070827-preview.zip +citelementtree==1.2.7 httplib2==0.11.3 lxml==4.2.1 nose==1.3.7 diff --git a/dist/Sharpy-0.9.7-py3.6.egg b/dist/Sharpy-0.9.7-py3.6.egg index c8f431d7a2a1c0bad9cb9d1c1ffdaf9a082814a6..c4d453ed989dc2a4ba69613ea29cbb3b24a74631 100644 GIT binary patch delta 20226 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 Zzd=D826!0nVore*hLC88QF> diff --git a/setup.py b/setup.py index c90386e..179e566 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ packages=['sharpy'], license="BSD", long_description=open('README.rst').read(), - install_requires=['httplib2', 'elementtree', 'python-dateutil'], + install_requires=['httplib2', 'citelementtree', 'python-dateutil'], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', From df98c29adc7bf52a489e22c4a0e809f6c4585671 Mon Sep 17 00:00:00 2001 From: Tahir Ijaz Date: Thu, 12 Jul 2018 10:14:49 -0400 Subject: [PATCH 45/53] code to get a paginated json get_customers response from cheddat --- sharpy/client.py | 9 +++++++++ sharpy/product.py | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/sharpy/client.py b/sharpy/client.py index 8de7949..8029275 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -76,6 +76,15 @@ def format_date(self, to_format): str_dt = utc_value.strftime('%Y-%m-%d') return str_dt + def get_client(self): + return httplib2.Http(cache=self.cache, timeout=self.timeout) + + def get_auth_headers(self): + headers = {} + headers['Authorization'] = "Basic %s" % base64.standard_b64encode( + str.encode(self.username + ':' + self.password,'utf-8')).strip().decode("utf-8") + return headers + def make_request(self, path, params=None, data=None, method=None): ''' Makes a request to the cheddar api using the authentication and diff --git a/sharpy/product.py b/sharpy/product.py index d885ba5..1b8ba8b 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -233,7 +233,8 @@ def build_customer_post_data(self, code=None, first_name=None, return data - def get_customers(self, filter_data=None): + def get_customers(self, filter_data=None, page=None, per_page=100, + format="json", product_id=None): ''' Returns all customers. Sometimes they are too much and cause internal server errors on CG. API call permits post parameters for filtering @@ -247,6 +248,38 @@ def get_customers(self, filter_data=None): ("planCode[]": "100GB"), ("planCode[]": "200GB") ] ''' + + # If 'page' is present bypass sharpy and return paginated customer + # response + if page is not None: + + if product_id is None: + # product_id is required + return None + + method = 'GET' + body = None + + custom_client = self.client.get_client() + headers = self.client.get_auth_headers() + url = "https://www.getcheddar.com/admin/customers/search/" \ + "orderBy/id/orderByDirection/asc/perPage/{}/page/" \ + "{}/format/{}/" \ + "productId/{}".format( + per_page, + page, + format, + product_id + ) + + response, content = custom_client.request( + url, method, body=body, headers=headers) + + if response.status == 200: + return content + else: + return None + customers = [] try: From 5e16ca15ef29e26c467037f015a14689c2f3d7df Mon Sep 17 00:00:00 2001 From: Tahir Ijaz Date: Fri, 13 Jul 2018 13:21:39 -0400 Subject: [PATCH 46/53] using product_code instead of product_id --- sharpy/product.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sharpy/product.py b/sharpy/product.py index 1b8ba8b..9a05fee 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -234,7 +234,7 @@ def build_customer_post_data(self, code=None, first_name=None, return data def get_customers(self, filter_data=None, page=None, per_page=100, - format="json", product_id=None): + format="json", product_code=None): ''' Returns all customers. Sometimes they are too much and cause internal server errors on CG. API call permits post parameters for filtering @@ -265,11 +265,11 @@ def get_customers(self, filter_data=None, page=None, per_page=100, url = "https://www.getcheddar.com/admin/customers/search/" \ "orderBy/id/orderByDirection/asc/perPage/{}/page/" \ "{}/format/{}/" \ - "productId/{}".format( + "productCode/{}".format( per_page, page, format, - product_id + product_code ) response, content = custom_client.request( From 56b02e0d07e5bb9b086e45e734a8543a45c90219 Mon Sep 17 00:00:00 2001 From: Tahir Ijaz Date: Mon, 23 Jul 2018 17:01:22 -0400 Subject: [PATCH 47/53] fixed typo --- sharpy/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sharpy/product.py b/sharpy/product.py index 9a05fee..e1f9ce5 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -253,7 +253,7 @@ def get_customers(self, filter_data=None, page=None, per_page=100, # response if page is not None: - if product_id is None: + if product_code is None: # product_id is required return None From f15150593b8a1b8928d235283917cb4d5bf8e6ee Mon Sep 17 00:00:00 2001 From: Clifton Barnes Date: Thu, 2 Jan 2020 12:52:06 -0500 Subject: [PATCH 48/53] Fix tests --- sharpy/product.py | 2 +- tests/client_tests.py | 9 +++++---- tests/parser_tests.py | 2 +- tests/product_tests.py | 6 +++--- tests/testing_tools/decorators.py | 2 +- tests/testing_tools/utils.py | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/sharpy/product.py b/sharpy/product.py index e1f9ce5..66336ff 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -871,7 +871,7 @@ def __init__(self, id=None, code=None, name=None, description=None, def __repr__(self): return 'Promotion: %s (%s)' % (self.name, self.code,) - def __unicode__(self): + def __str__(self): return '{0} ({1})'.format(self.name, self.code) def load_data(self, id=None, code=None, name=None, description=None, diff --git a/tests/client_tests.py b/tests/client_tests.py index d7eca68..5ffc975 100644 --- a/tests/client_tests.py +++ b/tests/client_tests.py @@ -16,7 +16,7 @@ from sharpy.exceptions import PreconditionFailed from sharpy.exceptions import UnprocessableEntity -from testing_tools.decorators import clear_users +from .testing_tools.decorators import clear_users class ClientTests(unittest.TestCase): @@ -110,10 +110,11 @@ def test_make_request_access_denied(self): @raises(BadRequest) def test_make_request_bad_request(self): - ''' Attempt to grab the plans without adding /get to the url. ''' - path = 'plans' + ''' Attempt to add customer without data. ''' + path = 'customers/new' + data = {} client = self.get_client() - client.make_request(path) + client.make_request(path, data=data) @raises(NotFound) def test_make_request_not_found(self): diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 3d0b399..2e531dc 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -19,7 +19,7 @@ class ParserTests(unittest.TestCase): def load_file(self, filename): ''' Helper method to load an xml file from the files directory. ''' path = os.path.join(os.path.dirname(__file__), 'files', filename) - f = open(path) + f = open(path, 'rb') content = f.read() f.close() return content diff --git a/tests/product_tests.py b/tests/product_tests.py index 9afcc54..70812ac 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -14,7 +14,7 @@ from sharpy.product import CheddarProduct from sharpy.exceptions import NotFound -from testing_tools.decorators import clear_users +from .testing_tools.decorators import clear_users class ProductTests(unittest.TestCase): @@ -726,7 +726,7 @@ def test_get_promotion(self): product = self.get_product() promotion = product.get_promotion('COUPON') - self.assertEqual(unicode(promotion), 'Coupon (COUPON)') + self.assertEqual(str(promotion), 'Coupon (COUPON)') self.assertEqual(promotion.name, 'Coupon') self.assertEqual(promotion.coupons[0].get('code'), 'COUPON') self.assertEqual(promotion.incentives[0].get('percentage'), '10') @@ -749,6 +749,6 @@ def test_promotion_unicode(self): promotion = product.get_promotion('COUPON') expected = 'Coupon (COUPON)' - result = unicode(promotion) + result = str(promotion) self.assertEquals(expected, result) diff --git a/tests/testing_tools/decorators.py b/tests/testing_tools/decorators.py index 39dc774..70caace 100644 --- a/tests/testing_tools/decorators.py +++ b/tests/testing_tools/decorators.py @@ -1,6 +1,6 @@ from nose.tools import make_decorator -from utils import clear_users as clear_users_func +from .utils import clear_users as clear_users_func def clear_users(func): diff --git a/tests/testing_tools/utils.py b/tests/testing_tools/utils.py index 909a281..f51c2b6 100644 --- a/tests/testing_tools/utils.py +++ b/tests/testing_tools/utils.py @@ -1,7 +1,7 @@ import httplib2 from time import time -from tests.testconfig import config +from testconfig import config def clear_users(): @@ -19,7 +19,7 @@ def clear_users(): response, content = h.request(url, 'POST') - if response.status != 200 or 'success' not in content: + if response.status != 200 or b'success' not in content: raise Exception( 'Could not clear users. Recieved a response of %s %s \n %s' % ( response.status, response.reason, content)) From 98aa82b25493cdf4b35e46707a5e94f0738da10c Mon Sep 17 00:00:00 2001 From: Clifton Barnes Date: Thu, 2 Jan 2020 13:11:55 -0500 Subject: [PATCH 49/53] Add Travis CI testing --- .travis.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f4aade2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python +python: + - "3.6" + - "3.7" + - "3.8" +env: + - secure: "qSOQ4EoedKr6NoIuFVmMqhsTxO6zngchbm2uo3Z0vNcAGYVUxKSPDuI/1vjVZZQyP//AyE3Bk2sfiUQUm05ZixqkBocX4CjwESWF/2aEKu1MNXkSpB5Ypn8W3w4cNuy8taT/Wws2XSc7C7Oc/XuLtwZVxQisPh01hwevF496xP2InPhO2ziLqUj4TkAx2pjHSjWXfj68pyTyflkwhRReqQsqPKMFaESKdngYk06cnOdBElk4YvNLJZnAbPXxhEAmz8sY3AC67MEFALmthnm2WfLmVSVQ8kp1yA0Pedlt1xvGh+YO4jkZTw5QrM4dhbA+DTdRt67U3PVDEUg3W+MrBb91PeubveJ3TXY6UXtbc2KXAh48yfc+veB4r0anlmVv19VDWDGqUJFzmtJRrbX9UhvKNh62P5JPDQqlATnEd08BZrtMBxn3gc5HLw097qvDo4DmvyP2cX2rbrk29lTJeUshol+TYnV/hYmJ0Bao8685I48rAW/ZRvZBw76549/f9e58kzY0dEcrDLQ0qHmOOJumPYSBp5wZ6nDfikGOie8z88mxxu7Blz7a09ZmH/f2uCMoAr+4vUU5HrLPaGJexGn718ZxvVLuY/qhDilIDtL+l/qXMXA2cCChKOjmWi8PrN9bNkt/QLEQj3Ke0vMyjeX6eLjhIAfiqzKBTSM/UGw=" + - secure: "A0SM/sb8fBHQBag07DxICRzgvmyogFF9xwtXwS63CSzekLhe9sqZF4KShCDhCOk8rbwtSOBJ96z4Hl2Bq/y8DBVwaMV0x9lX5eDi1wWEDBTHqV5Dtw6Y2S0hCfEKYdiIy2Byt3qXRZ9+bumi3llb4z7A4Jg1Kb2+MzqhY7qjFsc8ZZu33eqnQMnCQ2zmRy332xaiV77/jjMn/buMTZEE9hwXCvRT3mtYYak7OR7jTxMbQKOLG+TlOsGO9+uuY2xigBatY1sLSq5tQwrw6dgZS7D8QYQ4rKdaG4DjoE0UcoWsLw5IGMhxFuEnIbnjvLyej2eZnwZtlGnN5wyHbOj2/Cc/tkV46toFGyqeEvmEZEecKx1muamOdK4ZLEBr+5qZVD71d+j4H1q/5bvZsJ+gLSoOxVnha/B1vW4JkVZP1BDygd1ml/DF8XjdmLwYV6nT7ArjWpPFKPIejZil9AlY88J0xo3DSpw5C4AYBJLKDnI+ui0LGkrCt4rEUKojd2OGd9inTso3dTuRBQuEyr+wiCSsbOiROimM3kANoiSUg+dw9urwPyb9wKkqQ0UhNWSLGdxPZLvTWTajwv8HMM63MK/xTsqr9KejRXA1ibaic3Es/bFD1LZmzPkFLOweLtaqYr5c/ye5wDin22GkTW4jojA85ELfi8RR8fle2LAOsCQ=" + - secure: "ha/L3Wwd3eLW/r75au906RUwrR9KZn6OuHy/w4xTKmHirk5oTyHo+XbsthSHs2Xis0RfcZ+Zb862L75psS6C/PdZ0Xb8tg9LbcBausVRMdzvykgsH2DOavvTJs778OOsHU1uF0geOKNo4FJv5onFjMtQ8UZW05kP1LQAu/6BdKEo+CfBfKr5uIRfDaTY+NqRhrIZsGbX/1rD1T5jqOZxXO3tj/NxIBrqTDz/4He7mGQbDR1pwrWrTTdPkPV2xLGfTiTbBbigRi675MimtCz8eBERkLjgb3BRpsjcAtNYPrZySIzsaHeSxeNFBDfoBmJ3ufeBQLx0umSKU++O7JRB1j0lhHczj/YokjHSz5KxnYZc/MXWghjA8g+cIeg9xa//ev+vuqzhH+8hiaqn6bq55ht9ZMPbDUPqQrSZldaVs4Bb1926d7QbhH8RxcfD/sb69Febl0BBpYpS+RqM6tGHw42fL3sinzdkFESgNv8/PjWWwFdIMBGYaXnXM7TyhnnQZaPRFkPf3eqapIg8Q84C3zI9lSWFq5q3oVlQ4fiyMwFeLzrelpb94ArOU95EkEGvcDz06vvjgdmhvP0eitF5aNoy2vJuarDnxRWdoafLL1ilU2PrXdov55Y1vL+7MwmE8JbnnsSh/LwPKOiG2oiEQiDPcJMp6NgXwhDguR9UFAA=" +install: + - pip install -r dev-requirements.txt +script: + - cd tests + - nosetests --tc=cheddar.username:$USERNAME --tc=cheddar.password:$PASSWORD --tc=cheddar.product_code:$PRODUCT --tc=cheddar.endpoint:https://cheddargetter.com/xml From 3b27e56fc772503490c223cd231a3011a3056b48 Mon Sep 17 00:00:00 2001 From: Clifton Barnes Date: Thu, 2 Jan 2020 13:20:27 -0500 Subject: [PATCH 50/53] Send all variables to the job --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index f4aade2..7920ee6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,10 @@ python: - "3.7" - "3.8" env: - - secure: "qSOQ4EoedKr6NoIuFVmMqhsTxO6zngchbm2uo3Z0vNcAGYVUxKSPDuI/1vjVZZQyP//AyE3Bk2sfiUQUm05ZixqkBocX4CjwESWF/2aEKu1MNXkSpB5Ypn8W3w4cNuy8taT/Wws2XSc7C7Oc/XuLtwZVxQisPh01hwevF496xP2InPhO2ziLqUj4TkAx2pjHSjWXfj68pyTyflkwhRReqQsqPKMFaESKdngYk06cnOdBElk4YvNLJZnAbPXxhEAmz8sY3AC67MEFALmthnm2WfLmVSVQ8kp1yA0Pedlt1xvGh+YO4jkZTw5QrM4dhbA+DTdRt67U3PVDEUg3W+MrBb91PeubveJ3TXY6UXtbc2KXAh48yfc+veB4r0anlmVv19VDWDGqUJFzmtJRrbX9UhvKNh62P5JPDQqlATnEd08BZrtMBxn3gc5HLw097qvDo4DmvyP2cX2rbrk29lTJeUshol+TYnV/hYmJ0Bao8685I48rAW/ZRvZBw76549/f9e58kzY0dEcrDLQ0qHmOOJumPYSBp5wZ6nDfikGOie8z88mxxu7Blz7a09ZmH/f2uCMoAr+4vUU5HrLPaGJexGn718ZxvVLuY/qhDilIDtL+l/qXMXA2cCChKOjmWi8PrN9bNkt/QLEQj3Ke0vMyjeX6eLjhIAfiqzKBTSM/UGw=" - - secure: "A0SM/sb8fBHQBag07DxICRzgvmyogFF9xwtXwS63CSzekLhe9sqZF4KShCDhCOk8rbwtSOBJ96z4Hl2Bq/y8DBVwaMV0x9lX5eDi1wWEDBTHqV5Dtw6Y2S0hCfEKYdiIy2Byt3qXRZ9+bumi3llb4z7A4Jg1Kb2+MzqhY7qjFsc8ZZu33eqnQMnCQ2zmRy332xaiV77/jjMn/buMTZEE9hwXCvRT3mtYYak7OR7jTxMbQKOLG+TlOsGO9+uuY2xigBatY1sLSq5tQwrw6dgZS7D8QYQ4rKdaG4DjoE0UcoWsLw5IGMhxFuEnIbnjvLyej2eZnwZtlGnN5wyHbOj2/Cc/tkV46toFGyqeEvmEZEecKx1muamOdK4ZLEBr+5qZVD71d+j4H1q/5bvZsJ+gLSoOxVnha/B1vW4JkVZP1BDygd1ml/DF8XjdmLwYV6nT7ArjWpPFKPIejZil9AlY88J0xo3DSpw5C4AYBJLKDnI+ui0LGkrCt4rEUKojd2OGd9inTso3dTuRBQuEyr+wiCSsbOiROimM3kANoiSUg+dw9urwPyb9wKkqQ0UhNWSLGdxPZLvTWTajwv8HMM63MK/xTsqr9KejRXA1ibaic3Es/bFD1LZmzPkFLOweLtaqYr5c/ye5wDin22GkTW4jojA85ELfi8RR8fle2LAOsCQ=" - - secure: "ha/L3Wwd3eLW/r75au906RUwrR9KZn6OuHy/w4xTKmHirk5oTyHo+XbsthSHs2Xis0RfcZ+Zb862L75psS6C/PdZ0Xb8tg9LbcBausVRMdzvykgsH2DOavvTJs778OOsHU1uF0geOKNo4FJv5onFjMtQ8UZW05kP1LQAu/6BdKEo+CfBfKr5uIRfDaTY+NqRhrIZsGbX/1rD1T5jqOZxXO3tj/NxIBrqTDz/4He7mGQbDR1pwrWrTTdPkPV2xLGfTiTbBbigRi675MimtCz8eBERkLjgb3BRpsjcAtNYPrZySIzsaHeSxeNFBDfoBmJ3ufeBQLx0umSKU++O7JRB1j0lhHczj/YokjHSz5KxnYZc/MXWghjA8g+cIeg9xa//ev+vuqzhH+8hiaqn6bq55ht9ZMPbDUPqQrSZldaVs4Bb1926d7QbhH8RxcfD/sb69Febl0BBpYpS+RqM6tGHw42fL3sinzdkFESgNv8/PjWWwFdIMBGYaXnXM7TyhnnQZaPRFkPf3eqapIg8Q84C3zI9lSWFq5q3oVlQ4fiyMwFeLzrelpb94ArOU95EkEGvcDz06vvjgdmhvP0eitF5aNoy2vJuarDnxRWdoafLL1ilU2PrXdov55Y1vL+7MwmE8JbnnsSh/LwPKOiG2oiEQiDPcJMp6NgXwhDguR9UFAA=" + global: + - secure: "qSOQ4EoedKr6NoIuFVmMqhsTxO6zngchbm2uo3Z0vNcAGYVUxKSPDuI/1vjVZZQyP//AyE3Bk2sfiUQUm05ZixqkBocX4CjwESWF/2aEKu1MNXkSpB5Ypn8W3w4cNuy8taT/Wws2XSc7C7Oc/XuLtwZVxQisPh01hwevF496xP2InPhO2ziLqUj4TkAx2pjHSjWXfj68pyTyflkwhRReqQsqPKMFaESKdngYk06cnOdBElk4YvNLJZnAbPXxhEAmz8sY3AC67MEFALmthnm2WfLmVSVQ8kp1yA0Pedlt1xvGh+YO4jkZTw5QrM4dhbA+DTdRt67U3PVDEUg3W+MrBb91PeubveJ3TXY6UXtbc2KXAh48yfc+veB4r0anlmVv19VDWDGqUJFzmtJRrbX9UhvKNh62P5JPDQqlATnEd08BZrtMBxn3gc5HLw097qvDo4DmvyP2cX2rbrk29lTJeUshol+TYnV/hYmJ0Bao8685I48rAW/ZRvZBw76549/f9e58kzY0dEcrDLQ0qHmOOJumPYSBp5wZ6nDfikGOie8z88mxxu7Blz7a09ZmH/f2uCMoAr+4vUU5HrLPaGJexGn718ZxvVLuY/qhDilIDtL+l/qXMXA2cCChKOjmWi8PrN9bNkt/QLEQj3Ke0vMyjeX6eLjhIAfiqzKBTSM/UGw=" + - secure: "A0SM/sb8fBHQBag07DxICRzgvmyogFF9xwtXwS63CSzekLhe9sqZF4KShCDhCOk8rbwtSOBJ96z4Hl2Bq/y8DBVwaMV0x9lX5eDi1wWEDBTHqV5Dtw6Y2S0hCfEKYdiIy2Byt3qXRZ9+bumi3llb4z7A4Jg1Kb2+MzqhY7qjFsc8ZZu33eqnQMnCQ2zmRy332xaiV77/jjMn/buMTZEE9hwXCvRT3mtYYak7OR7jTxMbQKOLG+TlOsGO9+uuY2xigBatY1sLSq5tQwrw6dgZS7D8QYQ4rKdaG4DjoE0UcoWsLw5IGMhxFuEnIbnjvLyej2eZnwZtlGnN5wyHbOj2/Cc/tkV46toFGyqeEvmEZEecKx1muamOdK4ZLEBr+5qZVD71d+j4H1q/5bvZsJ+gLSoOxVnha/B1vW4JkVZP1BDygd1ml/DF8XjdmLwYV6nT7ArjWpPFKPIejZil9AlY88J0xo3DSpw5C4AYBJLKDnI+ui0LGkrCt4rEUKojd2OGd9inTso3dTuRBQuEyr+wiCSsbOiROimM3kANoiSUg+dw9urwPyb9wKkqQ0UhNWSLGdxPZLvTWTajwv8HMM63MK/xTsqr9KejRXA1ibaic3Es/bFD1LZmzPkFLOweLtaqYr5c/ye5wDin22GkTW4jojA85ELfi8RR8fle2LAOsCQ=" + - secure: "ha/L3Wwd3eLW/r75au906RUwrR9KZn6OuHy/w4xTKmHirk5oTyHo+XbsthSHs2Xis0RfcZ+Zb862L75psS6C/PdZ0Xb8tg9LbcBausVRMdzvykgsH2DOavvTJs778OOsHU1uF0geOKNo4FJv5onFjMtQ8UZW05kP1LQAu/6BdKEo+CfBfKr5uIRfDaTY+NqRhrIZsGbX/1rD1T5jqOZxXO3tj/NxIBrqTDz/4He7mGQbDR1pwrWrTTdPkPV2xLGfTiTbBbigRi675MimtCz8eBERkLjgb3BRpsjcAtNYPrZySIzsaHeSxeNFBDfoBmJ3ufeBQLx0umSKU++O7JRB1j0lhHczj/YokjHSz5KxnYZc/MXWghjA8g+cIeg9xa//ev+vuqzhH+8hiaqn6bq55ht9ZMPbDUPqQrSZldaVs4Bb1926d7QbhH8RxcfD/sb69Febl0BBpYpS+RqM6tGHw42fL3sinzdkFESgNv8/PjWWwFdIMBGYaXnXM7TyhnnQZaPRFkPf3eqapIg8Q84C3zI9lSWFq5q3oVlQ4fiyMwFeLzrelpb94ArOU95EkEGvcDz06vvjgdmhvP0eitF5aNoy2vJuarDnxRWdoafLL1ilU2PrXdov55Y1vL+7MwmE8JbnnsSh/LwPKOiG2oiEQiDPcJMp6NgXwhDguR9UFAA=" install: - pip install -r dev-requirements.txt script: From b1dee2e9dc63b9e7c355ae7b937e4aae31a0eb28 Mon Sep 17 00:00:00 2001 From: Clifton Barnes Date: Thu, 2 Jan 2020 13:26:14 -0500 Subject: [PATCH 51/53] Only test in python 3.6 --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7920ee6..3420312 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - "3.6" - - "3.7" - - "3.8" env: global: - secure: "qSOQ4EoedKr6NoIuFVmMqhsTxO6zngchbm2uo3Z0vNcAGYVUxKSPDuI/1vjVZZQyP//AyE3Bk2sfiUQUm05ZixqkBocX4CjwESWF/2aEKu1MNXkSpB5Ypn8W3w4cNuy8taT/Wws2XSc7C7Oc/XuLtwZVxQisPh01hwevF496xP2InPhO2ziLqUj4TkAx2pjHSjWXfj68pyTyflkwhRReqQsqPKMFaESKdngYk06cnOdBElk4YvNLJZnAbPXxhEAmz8sY3AC67MEFALmthnm2WfLmVSVQ8kp1yA0Pedlt1xvGh+YO4jkZTw5QrM4dhbA+DTdRt67U3PVDEUg3W+MrBb91PeubveJ3TXY6UXtbc2KXAh48yfc+veB4r0anlmVv19VDWDGqUJFzmtJRrbX9UhvKNh62P5JPDQqlATnEd08BZrtMBxn3gc5HLw097qvDo4DmvyP2cX2rbrk29lTJeUshol+TYnV/hYmJ0Bao8685I48rAW/ZRvZBw76549/f9e58kzY0dEcrDLQ0qHmOOJumPYSBp5wZ6nDfikGOie8z88mxxu7Blz7a09ZmH/f2uCMoAr+4vUU5HrLPaGJexGn718ZxvVLuY/qhDilIDtL+l/qXMXA2cCChKOjmWi8PrN9bNkt/QLEQj3Ke0vMyjeX6eLjhIAfiqzKBTSM/UGw=" From 1067df99928594f79272813d85ba036a9e76178b Mon Sep 17 00:00:00 2001 From: Clifton Barnes Date: Thu, 2 Jan 2020 13:38:18 -0500 Subject: [PATCH 52/53] Fix encoding of credentials --- sharpy/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sharpy/client.py b/sharpy/client.py index 8029275..7244d21 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -82,7 +82,8 @@ def get_client(self): def get_auth_headers(self): headers = {} headers['Authorization'] = "Basic %s" % base64.standard_b64encode( - str.encode(self.username + ':' + self.password,'utf-8')).strip().decode("utf-8") + (self.username + ':' + self.password).encode('utf-8') + ).strip().decode("utf-8") return headers def make_request(self, path, params=None, data=None, method=None): @@ -124,7 +125,8 @@ def make_request(self, path, params=None, data=None, method=None): # immediately to save an http request. headers['Authorization'] = "Basic %s" % base64.standard_b64encode( - str.encode(self.username + ':' + self.password,'utf-8')).strip().decode("utf-8") + (self.username + ':' + self.password).encode('utf-8') + ).strip().decode("utf-8") # Make request response, content = h.request(url, method, body=body, headers=headers) status = response.status From 214dadb17fe79b0cde9888fbf95f41bbf285f78a Mon Sep 17 00:00:00 2001 From: Clifton Barnes Date: Thu, 2 Jan 2020 14:02:09 -0500 Subject: [PATCH 53/53] Remove built objects --- Sharpy.egg-info/PKG-INFO | 86 --- Sharpy.egg-info/SOURCES.txt | 43 -- Sharpy.egg-info/dependency_links.txt | 1 - Sharpy.egg-info/requires.txt | 3 - Sharpy.egg-info/top_level.txt | 1 - build/lib/sharpy/__init__.py | 1 - build/lib/sharpy/client.py | 144 ----- build/lib/sharpy/exceptions.py | 69 --- build/lib/sharpy/parsers.py | 458 -------------- build/lib/sharpy/product.py | 860 --------------------------- dist/Sharpy-0.9.7-py2.7.egg | Bin 30491 -> 0 bytes dist/Sharpy-0.9.7-py3.6.egg | Bin 30495 -> 0 bytes getCheddarCustomers.py | 19 - 13 files changed, 1685 deletions(-) delete mode 100644 Sharpy.egg-info/PKG-INFO delete mode 100644 Sharpy.egg-info/SOURCES.txt delete mode 100644 Sharpy.egg-info/dependency_links.txt delete mode 100644 Sharpy.egg-info/requires.txt delete mode 100644 Sharpy.egg-info/top_level.txt delete mode 100644 build/lib/sharpy/__init__.py delete mode 100644 build/lib/sharpy/client.py delete mode 100644 build/lib/sharpy/exceptions.py delete mode 100644 build/lib/sharpy/parsers.py delete mode 100644 build/lib/sharpy/product.py delete mode 100644 dist/Sharpy-0.9.7-py2.7.egg delete mode 100644 dist/Sharpy-0.9.7-py3.6.egg delete mode 100644 getCheddarCustomers.py diff --git a/Sharpy.egg-info/PKG-INFO b/Sharpy.egg-info/PKG-INFO deleted file mode 100644 index 083aefd..0000000 --- a/Sharpy.egg-info/PKG-INFO +++ /dev/null @@ -1,86 +0,0 @@ -Metadata-Version: 1.1 -Name: Sharpy -Version: 0.9.7 -Summary: Python client for the Cheddar Getter API (http://cheddargetter.com). -Home-page: https://github.com/Saaspire/sharpy -Author: Sean O'Connor -Author-email: sean@saaspire.com -License: BSD -Description: ====== - Sharpy - ====== - - Sharpy is a client for the Cheddar Getter (https://cheddargetter.com/) API. - Cheddar Getter is a great service for handling recurring and usage based - billing. - - There are some existing python Cheddar Getter clients but they have - significant licensing problems, packaging problems, bugs, and are only partial - implementations of the Cheddar Getter API. - - Sharpy offers a number of advantages: - - * Clear and simple BSD license. - * Both a high and low level API - Work with cheddar the way you want to. - * 100% test coverage. - * Proper packaging - Sharpy can be installed via easy_install and PIP. - * Implements almost all of the Cheddar Getter API (See TODOs below). - * Will have complete documentation soon. - - That all being said, sharpy is still very new and is likely to undergo some - significant API changes in the near future. The code should be fairly safe - to use as long as you understand that future releases may not be backwards - compatible. - - Getting Started - =============== - - To get started with Sharpy, simply install it like you would any other python - package - - .. code:: - - pip install sharpy - - Optionally, you can also install `lxml `_ on your - system for faster XML parsing. - - Once you have sharpy installed, checkout our `docs `_ - on how to use the library. - - Documentation - ============= - - Sharpy's documentation is available at `ReadTheDocs - `_ or in the ``docs`` directory of the project. - - Code - ==== - - You can checkout and download Sharpy's latest code at `Github - `_. - - Installing elementtree for Development and Unit Testing - ======================================================= - When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. - - .. code:: - - wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip - unzip elementtree-1.2.6-20050316.zip - cd elementtree-1.2.6-20050316/ - pip install . - - TODOs - ===== - - * Flesh out the documentation to cover the full API. - * Add support for the various filtering options in the `get_customers` call. - -Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta -Classifier: Environment :: Web Environment -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Programming Language :: Python -Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/Sharpy.egg-info/SOURCES.txt b/Sharpy.egg-info/SOURCES.txt deleted file mode 100644 index cac2056..0000000 --- a/Sharpy.egg-info/SOURCES.txt +++ /dev/null @@ -1,43 +0,0 @@ -LICENSE.txt -MANIFEST.in -README.rst -dev-requirements.txt -setup.cfg -setup.py -Sharpy.egg-info/PKG-INFO -Sharpy.egg-info/SOURCES.txt -Sharpy.egg-info/dependency_links.txt -Sharpy.egg-info/requires.txt -Sharpy.egg-info/top_level.txt -docs/conf.py -docs/development.rst -docs/examples.rst -docs/high_level_api.rst -docs/index.rst -docs/examples/customer_creation/charges.py -docs/examples/customer_creation/free.py -docs/examples/customer_creation/items.py -docs/examples/customer_creation/optional.py -docs/examples/customer_creation/paid.py -docs/examples/customer_creation/paypal.py -docs/examples/customer_fetch/all_customers.py -docs/examples/customer_fetch/get_customer.py -sharpy/__init__.py -sharpy/client.py -sharpy/exceptions.py -sharpy/parsers.py -sharpy/product.py -tests/__init__.py -tests/client_tests.py -tests/parser_tests.py -tests/product_tests.py -tests/files/customers-with-items.xml -tests/files/customers-without-items.xml -tests/files/error.xml -tests/files/paypal_customer.xml -tests/files/plans.xml -tests/files/plans_with_items.xml -tests/files/promotions.xml -tests/testing_tools/__init__.py -tests/testing_tools/decorators.py -tests/testing_tools/utils.py \ No newline at end of file diff --git a/Sharpy.egg-info/dependency_links.txt b/Sharpy.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/Sharpy.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Sharpy.egg-info/requires.txt b/Sharpy.egg-info/requires.txt deleted file mode 100644 index 0e17f1d..0000000 --- a/Sharpy.egg-info/requires.txt +++ /dev/null @@ -1,3 +0,0 @@ -httplib2 -citelementtree -python-dateutil diff --git a/Sharpy.egg-info/top_level.txt b/Sharpy.egg-info/top_level.txt deleted file mode 100644 index 814d393..0000000 --- a/Sharpy.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -sharpy diff --git a/build/lib/sharpy/__init__.py b/build/lib/sharpy/__init__.py deleted file mode 100644 index 6c6b026..0000000 --- a/build/lib/sharpy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -VERSION = (0, 9, 7) diff --git a/build/lib/sharpy/client.py b/build/lib/sharpy/client.py deleted file mode 100644 index 7e6c7a2..0000000 --- a/build/lib/sharpy/client.py +++ /dev/null @@ -1,144 +0,0 @@ -import base64 -import logging -from urllib.parse import urlencode -from dateutil.tz import tzutc -import httplib2 - -from sharpy.exceptions import AccessDenied -from sharpy.exceptions import BadRequest -from sharpy.exceptions import CheddarError -from sharpy.exceptions import CheddarFailure -from sharpy.exceptions import NaughtyGateway -from sharpy.exceptions import NotFound -from sharpy.exceptions import PreconditionFailed -from sharpy.exceptions import UnprocessableEntity - -client_log = logging.getLogger('SharpyClient') - - -class Client(object): - default_endpoint = 'https://cheddargetter.com/xml' - - def __init__(self, username, password, product_code, cache=None, - timeout=None, endpoint=None): - ''' - username - Your cheddargetter username (probably an email address) - password - Your cheddargetter password - product_code - The product code for the product you want to work with - cache - A file system path or an object which implements the httplib2 - cache API (optional) - timeout - Socket level timout in seconds (optional) - endpoint - An alternate API endpoint (optional) - ''' - self.username = username - self.password = password - self.product_code = product_code - self.endpoint = endpoint or self.default_endpoint - self.cache = cache - self.timeout = timeout - - super(Client, self).__init__() - - def build_url(self, path, params=None): - ''' - Constructs the url for a cheddar API resource - ''' - url = '%s/%s/productCode/%s' % ( - self.endpoint, - path, - self.product_code, - ) - if params: - for key, value in list(params.items()): - url = '%s/%s/%s' % (url, key, value) - - return url - - def format_datetime(self, to_format): - if to_format == 'now': - str_dt = to_format - else: - if getattr(to_format, 'tzinfo', None) is not None: - utc_value = to_format.astimezone(tzutc()) - else: - utc_value = to_format - str_dt = utc_value.strftime('%Y-%m-%dT%H:%M:%S+00:00') - return str_dt - - def format_date(self, to_format): - if to_format == 'now': - str_dt = to_format - else: - if getattr(to_format, 'tzinfo', None) is not None: - utc_value = to_format.astimezone(tzutc()) - else: - utc_value = to_format - str_dt = utc_value.strftime('%Y-%m-%d') - return str_dt - - def make_request(self, path, params=None, data=None, method=None): - ''' - Makes a request to the cheddar api using the authentication and - configuration settings available. - ''' - # Setup values - url = self.build_url(path, params) - client_log.debug('Requesting: %s' % url) - method = method or 'GET' - body = None - headers = {} - cleaned_data = None - - if data: - method = 'POST' - body = urlencode(data) - headers = { - 'content-type': - 'application/x-www-form-urlencoded; charset=UTF-8', - } - - # Clean credit card info from when the request gets logged - # (remove ccv and only show last four of card num) - cleaned_data = data.copy() - if 'subscription[ccCardCode]' in cleaned_data: - del cleaned_data['subscription[ccCardCode]'] - if 'subscription[ccNumber]' in cleaned_data: - ccNum = cleaned_data['subscription[ccNumber]'] - cleaned_data['subscription[ccNumber]'] = ccNum[-4:] - - client_log.debug('Request Method: %s' % method) - client_log.debug('Request Body (Cleaned Data): %s' % cleaned_data) - - # Setup http client - h = httplib2.Http(cache=self.cache, timeout=self.timeout) - # Skip the normal http client behavior and send auth headers - # immediately to save an http request. - headers['Authorization'] = "Basic %s" % base64.standard_b64encode( - self.username + ':' + self.password).strip() - - # Make request - response, content = h.request(url, method, body=body, headers=headers) - status = response.status - client_log.debug('Response Status: %d' % status) - client_log.debug('Response Content: %s' % content) - if status != 200 and status != 302: - exception_class = CheddarError - if status == 401: - exception_class = AccessDenied - elif status == 400: - exception_class = BadRequest - elif status == 404: - exception_class = NotFound - elif status == 412: - exception_class = PreconditionFailed - elif status == 500: - exception_class = CheddarFailure - elif status == 502: - exception_class = NaughtyGateway - elif status == 422: - exception_class = UnprocessableEntity - - raise exception_class(response, content) - - response.content = content - return response diff --git a/build/lib/sharpy/exceptions.py b/build/lib/sharpy/exceptions.py deleted file mode 100644 index ab2cf17..0000000 --- a/build/lib/sharpy/exceptions.py +++ /dev/null @@ -1,69 +0,0 @@ - - -class CheddarError(Exception): - "Base class for exceptions returned by cheddar" - - def __init__(self, response, content, *args, **kwargs): - # Importing in method to break circular dependecy - from sharpy.parsers import parse_error - - super(CheddarError, self).__init__(*args, **kwargs) - error_info = parse_error(content) - self.response = response - self.error_info = error_info - - def __str__(self): - return '%s (%s) %s - %s' % ( - self.response.status, - self.error_info['aux_code'], - self.response.reason, - self.error_info['message'], - ) - - -class AccessDenied(CheddarError): - "A request to cheddar returned a status code of 401" - pass - - -class BadRequest(CheddarError): - "A request to cheddar was invalid in some way" - pass - - -class NotFound(CheddarError): - "A request to chedder was made for a resource which doesn't exist" - pass - - -class CheddarFailure(CheddarError): - "A request to cheddar encountered an unexpected error on the cheddar side" - pass - - -class PreconditionFailed(CheddarError): - "A request to cheddar was made but failed CG's validation in some way." - pass - - -class NaughtyGateway(CheddarError): - """ - Cheddar either couldn't contact the gateway or the gateway did something - very unexpected. - """ - pass - - -class UnprocessableEntity(CheddarError): - """ - An error occurred during processing. Please fix the error and try again. - """ - pass - - -class ParseError(Exception): - """ - Sharpy recieved unknown output from cheddar and doesn't know what - to do with it. - """ - pass diff --git a/build/lib/sharpy/parsers.py b/build/lib/sharpy/parsers.py deleted file mode 100644 index d9760ab..0000000 --- a/build/lib/sharpy/parsers.py +++ /dev/null @@ -1,458 +0,0 @@ -from decimal import Decimal, InvalidOperation -import logging - -from dateutil import parser as date_parser - -try: - from lxml.etree import XML -except ImportError: - from elementtree.ElementTree import XML - -from sharpy.exceptions import ParseError - -client_log = logging.getLogger('SharpyClient') - - -def parse_error(xml_str): - error = {} - doc = XML(xml_str) - if doc.tag == 'error': - elem = doc - elif doc.tag == 'customers': - elem = doc.find('.//error') - else: - raise Exception("Can't find error element in '%s'" % xml_str) - client_log.debug(elem) - error['id'] = elem.attrib['id'] - error['code'] = elem.attrib['code'] - error['aux_code'] = elem.attrib['auxCode'] - error['message'] = elem.text - - return error - - -class CheddarOutputParser(object): - ''' - A utility class for parsing the various datatypes returned by the - cheddar api. - ''' - def parse_bool(self, content): - if content == '' and content is not None: - value = None - elif content == '1': - value = True - elif content == '0': - value = False - else: - raise ParseError("Can't parse '%s' as a bool." % content) - - return value - - def parse_int(self, content): - value = None - if content != '' and content is not None: - try: - value = int(content) - except ValueError: - raise ParseError("Can't parse '%s' as an int." % content) - - return value - - def parse_decimal(self, content): - value = None - if content != '' and content is not None: - try: - value = Decimal(content) - except InvalidOperation: - raise ParseError("Can't parse '%s' as a decimal." % content) - - return value - - def parse_datetime(self, content): - value = None - if content: - try: - value = date_parser.parse(content) - except ValueError: - raise ParseError("Can't parse '%s' as a datetime." % content) - - return value - - -class PlansParser(CheddarOutputParser): - ''' - A utility class for parsing cheddar's xml output for pricing plans. - ''' - def parse_xml(self, xml_str): - plans = [] - plans_xml = XML(xml_str) - for plan_xml in plans_xml: - plan = self.parse_plan(plan_xml) - plans.append(plan) - - return plans - - def parse_plan(self, plan_element): - plan = {} - plan['id'] = plan_element.attrib['id'] - plan['code'] = plan_element.attrib['code'] - plan['name'] = plan_element.findtext('name') - plan['description'] = plan_element.findtext('description') - plan['is_active'] = self.parse_bool(plan_element.findtext('isActive')) - plan['is_free'] = self.parse_bool(plan_element.findtext('isFree')) - plan['trial_days'] = self.parse_int(plan_element.findtext('trialDays')) - plan['initial_bill_count'] = self.parse_int(plan_element.findtext( - 'initialBillCount')) - plan['initial_bill_count_unit'] = plan_element.findtext( - 'initialBillCountUnit') - plan['billing_frequency'] = plan_element.findtext('billingFrequency') - plan['billing_frequency_per'] = plan_element.findtext( - 'billingFrequencyPer') - plan['billing_frequency_unit'] = plan_element.findtext( - 'billingFrequencyUnit') - plan['billing_frequency_quantity'] = self.parse_int( - plan_element.findtext('billingFrequencyQuantity')) - plan['setup_charge_code'] = plan_element.findtext('setupChargeCode') - plan['setup_charge_amount'] = self.parse_decimal( - plan_element.findtext('setupChargeAmount')) - plan['recurring_charge_code'] = plan_element.findtext( - 'recurringChargeCode') - plan['recurring_charge_amount'] = self.parse_decimal( - plan_element.findtext('recurringChargeAmount')) - plan['created_datetime'] = self.parse_datetime( - plan_element.findtext('createdDatetime')) - - plan['items'] = self.parse_plan_items(plan_element.find('items')) - - return plan - - def parse_plan_items(self, items_element): - items = [] - - if items_element is not None: - for item_element in items_element: - items.append(self.parse_plan_item(item_element)) - - return items - - def parse_plan_item(self, item_element): - item = {} - - item['id'] = item_element.attrib['id'] - item['code'] = item_element.attrib['code'] - item['name'] = item_element.findtext('name') - item['quantity_included'] = self.parse_decimal( - item_element.findtext('quantityIncluded')) - item['is_periodic'] = self.parse_bool( - item_element.findtext('isPeriodic')) - item['overage_amount'] = self.parse_decimal( - item_element.findtext('overageAmount')) - item['created_datetime'] = self.parse_datetime( - item_element.findtext('createdDatetime')) - - return item - - -class CustomersParser(CheddarOutputParser): - ''' - Utility class for parsing cheddar's xml output for customers. - ''' - def parse_xml(self, xml_str): - customers = [] - customers_xml = XML(xml_str) - for customer_xml in customers_xml: - customer = self.parse_customer(customer_xml) - customers.append(customer) - - return customers - - def parse_customer(self, customer_element): - customer = {} - - # Basic info - customer['id'] = customer_element.attrib['id'] - customer['code'] = customer_element.attrib['code'] - customer['first_name'] = customer_element.findtext('firstName') - customer['last_name'] = customer_element.findtext('lastName') - customer['company'] = customer_element.findtext('company') - customer['email'] = customer_element.findtext('email') - customer['notes'] = customer_element.findtext('notes') - customer['gateway_token'] = customer_element.findtext('gateway_token') - customer['is_vat_exempt'] = customer_element.findtext('isVatExempt') - customer['vat_number'] = customer_element.findtext('vatNumber') - customer['first_contact_datetime'] = self.parse_datetime( - customer_element.findtext('firstContactDatetime')) - customer['referer'] = customer_element.findtext('referer') - customer['referer_host'] = customer_element.findtext('refererHost') - customer['campaign_source'] = customer_element.findtext( - 'campaignSource') - customer['campaign_medium'] = customer_element.findtext( - 'campaignMedium') - customer['campaign_term'] = customer_element.findtext('campaignTerm') - customer['campaign_content'] = customer_element.findtext( - 'campaignContent') - customer['campaign_name'] = customer_element.findtext('campaignName') - customer['created_datetime'] = self.parse_datetime( - customer_element.findtext('createdDatetime')) - customer['modified_datetime'] = self.parse_datetime( - customer_element.findtext('modifiedDatetime')) - - # Metadata - customer['meta_data'] = self.parse_meta_data( - customer_element.find('metaData')) - - # Subscriptions - customer['subscriptions'] = self.parse_subscriptions( - customer_element.find('subscriptions')) - - return customer - - def parse_meta_data(self, meta_data_element): - meta_data = [] - for meta_datum_element in meta_data_element: - meta_data.append(self.parse_meta_datum(meta_datum_element)) - - return meta_data - - def parse_meta_datum(self, meta_datum_element): - meta_datum = {} - - meta_datum['id'] = meta_datum_element.attrib['id'] - meta_datum['name'] = meta_datum_element.findtext('name') - meta_datum['value'] = meta_datum_element.findtext('value') - meta_datum['created_datetime'] = self.parse_datetime( - meta_datum_element.findtext('createdDatetime')) - meta_datum['modified_datetime'] = self.parse_datetime( - meta_datum_element.findtext('modifiedDatetime')) - - return meta_datum - - def parse_subscriptions(self, subscriptions_element): - subscriptions = [] - for subscription_element in subscriptions_element: - subscription = self.parse_subscription(subscription_element) - subscriptions.append(subscription) - - return subscriptions - - def parse_subscription(self, subscription_element): - subscription = {} - - # Basic info - subscription['id'] = subscription_element.attrib['id'] - subscription['gateway_token'] = subscription_element.findtext( - 'gatewayToken') - subscription['cc_first_name'] = subscription_element.findtext( - 'ccFirstName') - subscription['cc_last_name'] = subscription_element.findtext( - 'ccLastName') - subscription['cc_company'] = subscription_element.findtext('ccCompany') - subscription['cc_country'] = subscription_element.findtext('ccCountry') - subscription['cc_address'] = subscription_element.findtext('ccAddress') - subscription['cc_city'] = subscription_element.findtext('ccCity') - subscription['cc_state'] = subscription_element.findtext('ccState') - subscription['cc_zip'] = subscription_element.findtext('ccZip') - subscription['cc_type'] = subscription_element.findtext('ccType') - subscription['cc_email'] = subscription_element.findtext('ccEmail') - subscription['cc_last_four'] = subscription_element.findtext( - 'ccLastFour') - subscription['cc_expiration_date'] = subscription_element.findtext( - 'ccExpirationDate') - subscription['cancel_type'] = subscription_element.findtext( - 'cancelType') - subscription['cancel_reason'] = subscription_element.findtext( - 'cancelReason') - subscription['canceled_datetime'] = self.parse_datetime( - subscription_element.findtext('canceledDatetime')) - subscription['created_datetime'] = self.parse_datetime( - subscription_element.findtext('createdDatetime')) - subscription['coupon_code'] = subscription_element.findtext( - 'couponCode') - gateway_account_element = subscription_element.find('gatewayAccount') - if gateway_account_element is not None: - subscription['gateway_account'] = { - 'id': gateway_account_element.findtext('id'), - 'gateway': gateway_account_element.findtext('gateway'), - 'type': gateway_account_element.findtext('type') - } - subscription['redirect_url'] = subscription_element.findtext( - 'redirectUrl') - - # Plans - subscription['plans'] = self.parse_plans( - subscription_element.find('plans')) - - # Invoices - subscription['invoices'] = self.parse_invoices( - subscription_element.find('invoices')) - - subscription['items'] = self.parse_subscription_items( - subscription_element.find('items')) - - return subscription - - def parse_plans(self, plans_element): - plans_parser = PlansParser() - plans = [] - - if plans_element is not None: - for plan_element in plans_element: - plans.append(plans_parser.parse_plan(plan_element)) - - return plans - - def parse_invoices(self, invoices_element): - invoices = [] - if invoices_element is not None: - for invoice_element in invoices_element: - invoices.append(self.parse_invoice(invoice_element)) - - return invoices - - def parse_invoice(self, invoice_element): - invoice = {} - - invoice['id'] = invoice_element.attrib['id'] - invoice['number'] = invoice_element.findtext('number') - invoice['type'] = invoice_element.findtext('type') - invoice['vat_rate'] = invoice_element.findtext('vatRate') - invoice['billing_datetime'] = self.parse_datetime( - invoice_element.findtext('billingDatetime')) - invoice['paid_transaction_id'] = invoice_element.findtext( - 'paidTransactionId') - invoice['created_datetime'] = self.parse_datetime( - invoice_element.findtext('createdDatetime')) - - invoice['charges'] = self.parse_charges( - invoice_element.find('charges')) - - return invoice - - def parse_charges(self, charges_element): - charges = [] - - for charge_element in charges_element: - charges.append(self.parse_charge(charge_element)) - - return charges - - def parse_charge(self, charge_element): - charge = {} - - charge['id'] = charge_element.attrib['id'] - charge['code'] = charge_element.attrib['code'] - charge['type'] = charge_element.findtext('type') - charge['quantity'] = self.parse_decimal( - charge_element.findtext('quantity')) - charge['each_amount'] = self.parse_decimal( - charge_element.findtext('eachAmount')) - charge['description'] = charge_element.findtext('description') - charge['created_datetime'] = self.parse_datetime( - charge_element.findtext('createdDatetime')) - - return charge - - def parse_subscription_items(self, items_element): - items = [] - - if items_element is not None: - for item_element in items_element: - items.append(self.parse_subscription_item(item_element)) - - return items - - def parse_subscription_item(self, item_element): - item = {} - - item['id'] = item_element.attrib['id'] - item['code'] = item_element.attrib['code'] - item['name'] = item_element.findtext('name') - item['quantity'] = self.parse_decimal( - item_element.findtext('quantity')) - item['created_datetime'] = self.parse_datetime( - item_element.findtext('createdDatetime')) - item['modified_datetime'] = self.parse_datetime( - item_element.findtext('modifiedDatetime')) - - return item - - -class PromotionsParser(CheddarOutputParser): - ''' - A utility class for parsing cheddar's xml output for promotions. - ''' - def parse_xml(self, xml_str): - promotions = [] - promotions_xml = XML(xml_str) - for promotion_xml in promotions_xml: - promotion = self.parse_promotion(promotion_xml) - promotions.append(promotion) - - return promotions - - def parse_promotion(self, promotion_element): - promotion = {} - promotion['id'] = promotion_element.attrib['id'] - promotion['name'] = promotion_element.findtext('name') - promotion['description'] = promotion_element.findtext('description') - promotion['created_datetime'] = self.parse_datetime( - promotion_element.findtext('createdDatetime')) - - promotion['incentives'] = self.parse_incentives( - promotion_element.find('incentives')) - promotion['plans'] = self.parse_plans(promotion_element.find('plans')) - promotion['coupons'] = self.parse_coupons( - promotion_element.find('coupons')) - - return promotion - - def parse_incentives(self, incentives_element): - incentives = [] - - if incentives_element is not None: - for incentive_element in incentives_element: - incentives.append(self.parse_incentive(incentive_element)) - - return incentives - - def parse_incentive(self, incentive_element): - incentive = {} - - incentive['id'] = incentive_element.attrib['id'] - incentive['type'] = incentive_element.findtext('type') - incentive['percentage'] = incentive_element.findtext('percentage') - incentive['months'] = incentive_element.findtext('months') - incentive['created_datetime'] = self.parse_datetime( - incentive_element.findtext('createdDatetime')) - - return incentive - - def parse_plans(self, plans_element): - plans = [] - - if plans_element is not None: - for plan_element in plans_element: - plans.append(plan_element.findtext('code')) - return plans - - def parse_coupons(self, coupons_element): - coupons = [] - - if coupons_element is not None: - for coupon_element in coupons_element: - coupons.append(self.parse_coupon(coupon_element)) - - return coupons - - def parse_coupon(self, coupon_element): - coupon = {} - - coupon['id'] = coupon_element.attrib['id'] - coupon['code'] = coupon_element.attrib['code'] - coupon['max_redemptions'] = coupon_element.findtext('maxRedemptions') - coupon['expiration_datetime'] = self.parse_datetime( - coupon_element.findtext('expirationDatetime')) - coupon['created_datetime'] = self.parse_datetime( - coupon_element.findtext('createdDatetime')) - - return coupon diff --git a/build/lib/sharpy/product.py b/build/lib/sharpy/product.py deleted file mode 100644 index d885ba5..0000000 --- a/build/lib/sharpy/product.py +++ /dev/null @@ -1,860 +0,0 @@ -from copy import copy -from datetime import datetime -from decimal import Decimal -from time import time - - -from dateutil.relativedelta import relativedelta - -from sharpy.client import Client -from sharpy.exceptions import NotFound -from sharpy.parsers import PlansParser, CustomersParser, PromotionsParser - - -class CheddarProduct(object): - - def __init__(self, username, password, product_code, cache=None, - timeout=None, endpoint=None): - self.product_code = product_code - self.client = Client( - username, - password, - product_code, - cache, - timeout, - endpoint, - ) - - super(CheddarProduct, self).__init__() - - def __repr__(self): - return 'CheddarProduct: %s' % self.product_code - - def get_all_plans(self): - response = self.client.make_request(path='plans/get') - plans_parser = PlansParser() - plans_data = plans_parser.parse_xml(response.content) - plans = [PricingPlan(**plan_data) for plan_data in plans_data] - - return plans - - def get_plan(self, code): - response = self.client.make_request( - path='plans/get', - params={'code': code}, - ) - plans_parser = PlansParser() - plans_data = plans_parser.parse_xml(response.content) - plans = [PricingPlan(**plan_data) for plan_data in plans_data] - - return plans[0] - - def create_customer(self, code, first_name, last_name, email, plan_code, - company=None, is_vat_exempt=None, vat_number=None, - notes=None, first_contact_datetime=None, - referer=None, campaign_term=None, - campaign_name=None, campaign_source=None, - campaign_medium=None, campaign_content=None, - meta_data=None, initial_bill_date=None, method=None, - cc_number=None, cc_expiration=None, - cc_card_code=None, cc_first_name=None, - cc_last_name=None, cc_email=None, cc_company=None, - cc_country=None, cc_address=None, cc_city=None, - cc_state=None, cc_zip=None, coupon_code=None, - return_url=None, cancel_url=None, charges=None, - items=None): - - data = self.build_customer_post_data(code, first_name, last_name, - email, plan_code, company, - is_vat_exempt, vat_number, - notes, first_contact_datetime, - referer, campaign_term, - campaign_name, campaign_source, - campaign_medium, campaign_content, - meta_data, initial_bill_date, - method, cc_number, cc_expiration, - cc_card_code, cc_first_name, - cc_last_name, cc_email, - cc_company, cc_country, - cc_address, cc_city, cc_state, - cc_zip, coupon_code, return_url, - cancel_url) - - if charges: - for i, charge in enumerate(charges): - data['charges[%d][chargeCode]' % i] = charge['code'] - data['charges[%d][quantity]' % i] = charge.get('quantity', 1) - data['charges[%d][eachAmount]' % i] = '%.2f' % ( - charge['each_amount']) - data['charges[%d][description]' % i] = charge.get( - 'description', '') - - if items: - for i, item in enumerate(items): - data['items[%d][itemCode]' % i] = item['code'] - data['items[%d][quantity]' % i] = item.get('quantity', 1) - - response = self.client.make_request(path='customers/new', data=data) - customer_parser = CustomersParser() - customers_data = customer_parser.parse_xml(response.content) - customer = Customer(product=self, **customers_data[0]) - - return customer - - def build_customer_post_data(self, code=None, first_name=None, - last_name=None, email=None, plan_code=None, - company=None, is_vat_exempt=None, - vat_number=None, notes=None, - first_contact_datetime=None, referer=None, - campaign_term=None, campaign_name=None, - campaign_source=None, campaign_medium=None, - campaign_content=None, meta_data=None, - initial_bill_date=None, method=None, - cc_number=None, cc_expiration=None, - cc_card_code=None, cc_first_name=None, - cc_last_name=None, cc_email=None, - cc_company=None, cc_country=None, - cc_address=None, cc_city=None, - cc_state=None, cc_zip=None, coupon_code=None, - return_url=None, cancel_url=None, - bill_date=None): - - data = {} - - if code: - data['code'] = code - - if first_name: - data['firstName'] = first_name - - if last_name: - data['lastName'] = last_name - - if email: - data['email'] = email - - if plan_code: - data['subscription[planCode]'] = plan_code - - if company: - data['company'] = company - - if is_vat_exempt is not None: - if is_vat_exempt: - data['isVatExempt'] = 1 - else: - data['isVatExempt'] = 0 - - if vat_number: - data['vatNumber'] = vat_number - - if notes: - data['notes'] = notes - - if first_contact_datetime: - data['firstContactDatetime'] = self.client.format_datetime( - first_contact_datetime) - - if referer: - data['referer'] = referer - - if campaign_term: - data['campaignTerm'] = campaign_term - - if campaign_name: - data['campaignName'] = campaign_name - - if campaign_source: - data['campaignSource'] = campaign_source - - if campaign_content: - data['campaignContent'] = campaign_content - - if meta_data: - for key, value in list(meta_data.items()): - full_key = 'metaData[%s]' % key - data[full_key] = value - - if initial_bill_date: - data['subscription[initialBillDate]'] = self.client.format_date( - initial_bill_date) - - if method: - data['subscription[method]'] = method - - if cc_number: - data['subscription[ccNumber]'] = cc_number - - if cc_expiration: - data['subscription[ccExpiration]'] = cc_expiration - - if cc_card_code: - data['subscription[ccCardCode]'] = cc_card_code - - if cc_first_name: - data['subscription[ccFirstName]'] = cc_first_name - - if cc_last_name: - data['subscription[ccLastName]'] = cc_last_name - - if cc_email: - data['subscription[ccEmail]'] = cc_email - - if cc_company: - data['subscription[ccCompany]'] = cc_company - - if cc_country: - data['subscription[ccCountry]'] = cc_country - - if cc_address: - data['subscription[ccAddress]'] = cc_address - - if cc_city: - data['subscription[ccCity]'] = cc_city - - if cc_state: - data['subscription[ccState]'] = cc_state - - if cc_zip: - data['subscription[ccZip]'] = cc_zip - - if coupon_code: - data['subscription[couponCode]'] = coupon_code - - if return_url: - data['subscription[returnUrl]'] = return_url - - if cancel_url: - data['subscription[cancelUrl]'] = cancel_url - - if bill_date: - data['subscription[changeBillDate]'] = self.client.format_datetime( - bill_date) - - return data - - def get_customers(self, filter_data=None): - ''' - Returns all customers. Sometimes they are too much and cause internal - server errors on CG. API call permits post parameters for filtering - which tends to fix this - https://cheddargetter.com/developers#all-customers - - filter_data - Will be processed by urlencode and can be used for filtering - Example value: [ - ("subscriptionStatus": "activeOnly"), - ("planCode[]": "100GB"), ("planCode[]": "200GB") - ] - ''' - customers = [] - - try: - response = self.client.make_request(path='customers/get', - data=filter_data) - except NotFound: - response = None - - if response: - customer_parser = CustomersParser() - customers_data = customer_parser.parse_xml(response.content) - for customer_data in customers_data: - customers.append(Customer(product=self, **customer_data)) - - return customers - - def get_customer(self, code): - - response = self.client.make_request( - path='customers/get', - params={'code': code}, - ) - customer_parser = CustomersParser() - customers_data = customer_parser.parse_xml(response.content) - - return Customer(product=self, **customers_data[0]) - - def delete_all_customers(self): - ''' - This method does exactly what you think it does. Calling this method - deletes all customer data in your cheddar product and the configured - gateway. This action cannot be undone. - - DO NOT RUN THIS UNLESS YOU REALLY, REALLY, REALLY MEAN TO! - ''' - self.client.make_request( - path='customers/delete-all/confirm/%d' % int(time()), - method='POST' - ) - - def get_all_promotions(self): - ''' - Returns all promotions. - https://cheddargetter.com/developers#promotions - ''' - promotions = [] - - try: - response = self.client.make_request(path='promotions/get') - except NotFound: - response = None - - if response: - promotions_parser = PromotionsParser() - promotions_data = promotions_parser.parse_xml(response.content) - promotions = [Promotion(**promotion_data) for promotion_data in promotions_data] - - return promotions - - def get_promotion(self, code): - ''' - Get the promotion with the specified coupon code. - https://cheddargetter.com/developers#single-promotion - ''' - - response = self.client.make_request( - path='promotions/get', - params={'code': code}, - ) - promotion_parser = PromotionsParser() - promotion_data = promotion_parser.parse_xml(response.content) - - return Promotion(**promotion_data[0]) - - -class PricingPlan(object): - - def __init__(self, name, code, id, description, is_active, is_free, - trial_days, initial_bill_count, initial_bill_count_unit, - billing_frequency, billing_frequency_per, - billing_frequency_quantity, billing_frequency_unit, - setup_charge_code, setup_charge_amount, - recurring_charge_code, recurring_charge_amount, - created_datetime, items, subscription=None): - - self.load_data(name=name, code=code, id=id, description=description, - is_active=is_active, is_free=is_free, - trial_days=trial_days, - initial_bill_count=initial_bill_count, - initial_bill_count_unit=initial_bill_count_unit, - billing_frequency=billing_frequency, - billing_frequency_per=billing_frequency_per, - billing_frequency_quantity=billing_frequency_quantity, - billing_frequency_unit=billing_frequency_unit, - setup_charge_code=setup_charge_code, - setup_charge_amount=setup_charge_amount, - recurring_charge_code=recurring_charge_code, - recurring_charge_amount=recurring_charge_amount, - created_datetime=created_datetime, items=items, - subscription=subscription) - - super(PricingPlan, self).__init__() - - def load_data(self, name, code, id, description, is_active, is_free, - trial_days, initial_bill_count, initial_bill_count_unit, - billing_frequency, billing_frequency_per, - billing_frequency_quantity, billing_frequency_unit, - setup_charge_code, setup_charge_amount, - recurring_charge_code, recurring_charge_amount, - created_datetime, items, subscription=None): - - self.name = name - self.code = code - self.id = id - self.description = description - self.is_active = is_active - self.is_free = is_free - self.trial_days = trial_days - self.initial_bill_count = initial_bill_count - self.initial_bill_count_unit = initial_bill_count_unit - self.billing_frequency = billing_frequency - self.billing_frequency_per = billing_frequency_per - self.billing_frequency_quantity = billing_frequency_quantity - self.billing_frequency_unit = billing_frequency_unit - self.setup_charge_code = setup_charge_code - self.setup_charge_amount = setup_charge_amount - self.recurring_charge_code = recurring_charge_code - self.recurring_charge_amount = recurring_charge_amount - self.created = created_datetime - self.items = items - - if subscription: - self.subscription = subscription - - def __repr__(self): - return 'PricingPlan: %s (%s)' % (self.name, self.code) - - @property - def initial_bill_date(self): - ''' - An estimated initial bill date for an account created today, - based on available plan info. - ''' - time_to_start = None - - if self.initial_bill_count_unit == 'months': - time_to_start = relativedelta(months=self.initial_bill_count) - else: - time_to_start = relativedelta(days=self.initial_bill_count) - - initial_bill_date = datetime.utcnow().date() + time_to_start - - return initial_bill_date - - -class Customer(object): - - def __init__(self, code, first_name, last_name, email, product, id=None, - company=None, notes=None, gateway_token=None, - is_vat_exempt=None, vat_number=None, - first_contact_datetime=None, referer=None, - referer_host=None, campaign_source=None, - campaign_medium=None, campaign_term=None, - campaign_content=None, campaign_name=None, - created_datetime=None, modified_datetime=None, - coupon_code=None, meta_data=None, subscriptions=None): - - self.load_data(code=code, - first_name=first_name, last_name=last_name, - email=email, product=product, id=id, - company=company, notes=notes, - gateway_token=gateway_token, - is_vat_exempt=is_vat_exempt, - vat_number=vat_number, - first_contact_datetime=first_contact_datetime, - referer=referer, referer_host=referer_host, - campaign_source=campaign_source, - campaign_medium=campaign_medium, - campaign_term=campaign_term, - campaign_content=campaign_content, - campaign_name=campaign_name, - created_datetime=created_datetime, - modified_datetime=modified_datetime, - coupon_code=coupon_code, meta_data=meta_data, - subscriptions=subscriptions) - - super(Customer, self).__init__() - - def load_data(self, code, first_name, last_name, email, product, id=None, - company=None, notes=None, gateway_token=None, - is_vat_exempt=None, vat_number=None, - first_contact_datetime=None, referer=None, - referer_host=None, campaign_source=None, - campaign_medium=None, campaign_term=None, - campaign_content=None, campaign_name=None, - created_datetime=None, modified_datetime=None, - coupon_code=None, meta_data=None, subscriptions=None): - self.code = code - self.id = id - self.first_name = first_name - self.last_name = last_name - self.email = email - self.product = product - self.company = company - self.notes = notes - self.gateway_token = gateway_token - self.is_vat_exempt = is_vat_exempt - self.vat_number = vat_number - self.first_contact_datetime = first_contact_datetime - self.referer = referer - self.referer_host = referer_host - self.campaign_source = campaign_source - self.campaign_medium = campaign_medium - self.campaign_content = campaign_content - self.campaign_name = campaign_name - self.created = created_datetime - self.modified = modified_datetime - self.coupon_code = coupon_code - - self.meta_data = {} - if meta_data: - for datum in meta_data: - self.meta_data[datum['name']] = datum['value'] - subscription_data = subscriptions[0] - subscription_data['customer'] = self - if hasattr(self, 'subscription'): - self.subscription.load_data(**subscription_data) - else: - self.subscription = Subscription(**subscription_data) - - def load_data_from_xml(self, xml): - customer_parser = CustomersParser() - customers_data = customer_parser.parse_xml(xml) - customer_data = customers_data[0] - self.load_data(product=self.product, **customer_data) - - def update(self, first_name=None, last_name=None, email=None, - company=None, is_vat_exempt=None, vat_number=None, - notes=None, first_contact_datetime=None, - referer=None, campaign_term=None, campaign_name=None, - campaign_source=None, campaign_medium=None, - campaign_content=None, meta_data=None, method=None, - cc_number=None, cc_expiration=None, - cc_card_code=None, cc_first_name=None, - cc_last_name=None, cc_company=None, cc_email=None, - cc_country=None, cc_address=None, cc_city=None, - cc_state=None, cc_zip=None, plan_code=None, bill_date=None, - coupon_code=None, return_url=None, cancel_url=None,): - - data = self.product.build_customer_post_data( - first_name=first_name, last_name=last_name, email=email, - plan_code=plan_code, company=company, is_vat_exempt=is_vat_exempt, - vat_number=vat_number, notes=notes, referer=referer, - campaign_term=campaign_term, campaign_name=campaign_name, - campaign_source=campaign_source, campaign_medium=campaign_medium, - campaign_content=campaign_content, meta_data=meta_data, - method=method, cc_number=cc_number, cc_expiration=cc_expiration, - cc_card_code=cc_card_code, - cc_first_name=cc_first_name, cc_last_name=cc_last_name, - cc_company=cc_company, cc_email=cc_email, cc_country=cc_country, - cc_address=cc_address, cc_city=cc_city, cc_state=cc_state, - cc_zip=cc_zip, bill_date=bill_date, coupon_code=coupon_code, - return_url=return_url, cancel_url=cancel_url,) - - path = 'customers/edit' - params = {'code': self.code} - - response = self.product.client.make_request( - path=path, - params=params, - data=data, - ) - return self.load_data_from_xml(response.content) - - def delete(self): - path = 'customers/delete' - params = {'code': self.code} - self.product.client.make_request( - path=path, - params=params, - ) - - def charge(self, code, each_amount, quantity=1, description=None): - ''' - Add an arbitrary charge or credit to a customer's account. A positive - number will create a charge. A negative number will create a credit. - - each_amount is normalized to a Decimal with a precision of 2 as that - is the level of precision which the cheddar API supports. - ''' - each_amount = Decimal(each_amount) - each_amount = each_amount.quantize(Decimal('.01')) - data = { - 'chargeCode': code, - 'eachAmount': '%.2f' % each_amount, - 'quantity': quantity, - } - if description: - data['description'] = description - - response = self.product.client.make_request( - path='customers/add-charge', - params={'code': self.code}, - data=data, - ) - return self.load_data_from_xml(response.content) - - def create_one_time_invoice(self, charges): - ''' - Charges should be a list of charges to execute immediately. Each - value in the charges diectionary should be a dictionary with the - following keys: - - code - Your code for this charge. This code will be displayed in the - user's invoice and is limited to 36 characters. - quantity - A positive integer quantity. If not provided this value will - default to 1. - each_amount - Positive or negative integer or decimal with two digit precision. - A positive number will create a charge (debit). A negative number - will create a credit. - description - An optional description for this charge which will be displayed on - the user's invoice. - ''' - data = {} - for n, charge in enumerate(charges): - each_amount = Decimal(charge['each_amount']) - each_amount = each_amount.quantize(Decimal('.01')) - data['charges[%d][chargeCode]' % n] = charge['code'] - data['charges[%d][quantity]' % n] = charge.get('quantity', 1) - data['charges[%d][eachAmount]' % n] = '%.2f' % each_amount - if 'description' in list(charge.keys()): - data['charges[%d][description]' % n] = charge['description'] - - response = self.product.client.make_request( - path='invoices/new', - params={'code': self.code}, - data=data, - ) - return self.load_data_from_xml(response.content) - - def __repr__(self): - return 'Customer: %s %s (%s)' % ( - self.first_name, - self.last_name, - self.code - ) - - -class Subscription(object): - - def __init__(self, id, gateway_token, cc_first_name, cc_last_name, - cc_company, cc_country, cc_address, cc_city, cc_state, - cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, - canceled_datetime=None, created_datetime=None, - plans=None, invoices=None, items=None, gateway_account=None, - cancel_reason=None, cancel_type=None, cc_email=None, - coupon_code=None, redirect_url=None): - - self.load_data(id=id, gateway_token=gateway_token, - cc_first_name=cc_first_name, - cc_last_name=cc_last_name, - cc_company=cc_company, cc_country=cc_country, - cc_address=cc_address, cc_city=cc_city, - cc_state=cc_state, cc_zip=cc_zip, cc_type=cc_type, - cc_last_four=cc_last_four, - cc_expiration_date=cc_expiration_date, - cc_email=cc_email, customer=customer, - canceled_datetime=canceled_datetime, - created_datetime=created_datetime, plans=plans, - invoices=invoices, items=items, - gateway_account=gateway_account, - cancel_reason=cancel_reason, cancel_type=cancel_type, - coupon_code=coupon_code, redirect_url=redirect_url) - - super(Subscription, self).__init__() - - def load_data(self, id, gateway_token, cc_first_name, cc_last_name, - cc_company, cc_country, cc_address, cc_city, cc_state, - cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, - cc_email=None, canceled_datetime=None, created_datetime=None, - plans=None, invoices=None, items=None, gateway_account=None, - cancel_reason=None, cancel_type=None, coupon_code=None, - redirect_url=None): - - self.id = id - self.gateway_token = gateway_token - self.cc_first_name = cc_first_name - self.cc_last_name = cc_last_name - self.cc_company = cc_company - self.cc_country = cc_country - self.cc_address = cc_address - self.cc_city = cc_city - self.cc_state = cc_state - self.cc_zip = cc_zip - self.cc_type = cc_type - self.cc_last_four = cc_last_four - self.cc_expiration_date = cc_expiration_date - self.cc_email = cc_email - self.canceled = canceled_datetime - self.created = created_datetime - self.invoices = invoices - self.customer = customer - self.gateway_account = gateway_account - self.cancel_type = cancel_type - self.cancel_reason = cancel_reason - self.coupon_code = coupon_code - self.redirect_url = redirect_url - - # Organize item data into something more useful - items_map = {} - for item in items: - items_map[item['code']] = {'subscription_data': item} - plan_data = plans[0] - for item in plan_data['items']: - # A temporary bandage sometimes plan_data['items'] was None - try: - items_map[item['code']]['plan_data'] = item - except Exception: - pass - - if not hasattr(self, 'items'): - self.items = {} - for code, item_map in list(items_map.items()): - plan_item_data = item_map['plan_data'] - subscription_item_data = item_map['subscription_data'] - item_data = copy(plan_item_data) - item_data.update(subscription_item_data) - item_data['subscription'] = self - - if code in list(self.items.keys()): - item = self.items[code] - item.load_data(**item_data) - else: - self.items[code] = Item(**item_data) - - plan_data['subscription'] = self - if hasattr(self, 'plan'): - self.plan.load_data(**plan_data) - else: - self.plan = PricingPlan(**plan_data) - - def __repr__(self): - return 'Subscription: %s' % self.id - - def cancel(self): - client = self.customer.product.client - response = client.make_request( - path='customers/cancel', - params={'code': self.customer.code}, - ) - - customer_parser = CustomersParser() - customers_data = customer_parser.parse_xml(response.content) - customer_data = customers_data[0] - self.customer.load_data( - product=self.customer.product, - **customer_data - ) - - -class Item(object): - - def __init__(self, code, subscription, id=None, name=None, - quantity_included=None, is_periodic=None, - overage_amount=None, created_datetime=None, - modified_datetime=None, quantity=None): - - self.load_data(code=code, subscription=subscription, id=id, name=name, - quantity_included=quantity_included, - is_periodic=is_periodic, overage_amount=overage_amount, - created_datetime=created_datetime, - modified_datetime=modified_datetime, quantity=quantity) - - super(Item, self).__init__() - - def load_data(self, code, subscription, id=None, name=None, - quantity_included=None, is_periodic=None, - overage_amount=None, created_datetime=None, - modified_datetime=None, quantity=None): - - self.code = code - self.subscription = subscription - self.id = id - self.name = name - self.quantity_included = quantity_included - self.quantity_used = quantity - self.is_periodic = is_periodic - self.overage_amount = overage_amount - self.created = created_datetime - self.modified = modified_datetime - - def __repr__(self): - return 'Item: %s for %s' % ( - self.code, - self.subscription.customer.code, - ) - - def _normalize_quantity(self, quantity=None): - if quantity is not None: - quantity = Decimal(quantity) - quantity = quantity.quantize(Decimal('.0001')) - - return quantity - - def increment(self, quantity=None): - ''' - Increment the item's quantity by the passed in amount. If nothing is - passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of - precision which the Cheddar API accepts. - ''' - data = {} - if quantity: - data['quantity'] = self._normalize_quantity(quantity) - - response = self.subscription.customer.product.client.make_request( - path='customers/add-item-quantity', - params={ - 'code': self.subscription.customer.code, - 'itemCode': self.code, - }, - data=data, - method='POST', - ) - - return self.subscription.customer.load_data_from_xml(response.content) - - def decrement(self, quantity=None): - ''' - Decrement the item's quantity by the passed in amount. If nothing is - passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of - precision which the Cheddar API accepts. - ''' - data = {} - if quantity: - data['quantity'] = self._normalize_quantity(quantity) - - response = self.subscription.customer.product.client.make_request( - path='customers/remove-item-quantity', - params={ - 'code': self.subscription.customer.code, - 'itemCode': self.code, - }, - data=data, - method='POST', - ) - - return self.subscription.customer.load_data_from_xml(response.content) - - def set(self, quantity): - ''' - Set the item's quantity to the passed in amount. If nothing is - passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of - precision which the Cheddar API accepts. - ''' - data = {} - data['quantity'] = self._normalize_quantity(quantity) - - response = self.subscription.customer.product.client.make_request( - path='customers/set-item-quantity', - params={ - 'code': self.subscription.customer.code, - 'itemCode': self.code, - }, - data=data, - method='POST', - ) - - return self.subscription.customer.load_data_from_xml(response.content) - - -class Promotion(object): - def __init__(self, id=None, code=None, name=None, description=None, - created_datetime=None, incentives=None, plans=None, - coupons=None): - - self.load_data(code=code, id=id, name=name, description=description, - created_datetime=created_datetime, - incentives=incentives, plans=plans, coupons=coupons) - - super(Promotion, self).__init__() - - def __repr__(self): - return 'Promotion: %s (%s)' % (self.name, self.code,) - - def __unicode__(self): - return '{0} ({1})'.format(self.name, self.code) - - def load_data(self, id=None, code=None, name=None, description=None, - created_datetime=None, incentives=None, plans=None, - coupons=None): - - self.code = code - self.id = id - self.name = name - self.description = description - self.created = created_datetime - - self.plans = plans - self.incentives = incentives - self.coupons = coupons - - # Bring coupon code up to parent promotion - if self.code is None and self.coupons and len(self.coupons) > 0: - self.code = self.coupons[0].get('code') diff --git a/dist/Sharpy-0.9.7-py2.7.egg b/dist/Sharpy-0.9.7-py2.7.egg deleted file mode 100644 index 88fd089556cf8ffd84fb2d2eabeab16b1a6f1c21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30491 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 diff --git a/dist/Sharpy-0.9.7-py3.6.egg b/dist/Sharpy-0.9.7-py3.6.egg deleted file mode 100644 index c4d453ed989dc2a4ba69613ea29cbb3b24a74631..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30495 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 WzbQcf0?;7<1pkgg1~~ld(f