From 9f3f505906b1946f0484420c5b0126a33a42ee62 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Mon, 3 Apr 2017 13:31:03 -0500 Subject: [PATCH 1/5] Bump api v2.6 --- recurly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recurly/__init__.py b/recurly/__init__.py index 541921a9..7dc1c702 100644 --- a/recurly/__init__.py +++ b/recurly/__init__.py @@ -42,7 +42,7 @@ API_KEY = None """The API key to use when authenticating API requests.""" -API_VERSION = '2.5' +API_VERSION = '2.6' """The API version to use when making API requests.""" CA_CERTS_FILE = None From 6ec44745a47dedb445b3548ad4a3d0497284911d Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Mon, 3 Apr 2017 13:29:48 -0500 Subject: [PATCH 2/5] Cardless Free Trial changes for 2.6 --- recurly/__init__.py | 2 ++ tests/fixtures/plan/exists.xml | 1 + tests/fixtures/subscription/show.xml | 1 + 3 files changed, 4 insertions(+) diff --git a/recurly/__init__.py b/recurly/__init__.py index 7dc1c702..c6b5377e 100644 --- a/recurly/__init__.py +++ b/recurly/__init__.py @@ -859,6 +859,7 @@ class Subscription(Resource): 'shipping_address_id', 'started_with_gift', 'converted_at', + 'no_billing_info_reason', ) sensitive_attributes = ('number', 'verification_value', 'bulk') @@ -1094,6 +1095,7 @@ class Plan(Resource): 'total_billing_cycles', 'revenue_schedule_type', 'setup_fee_revenue_schedule_type', + 'trial_requires_billing_info', ) def get_add_on(self, add_on_code): diff --git a/tests/fixtures/plan/exists.xml b/tests/fixtures/plan/exists.xml index e3a4da13..1492f2ca 100644 --- a/tests/fixtures/plan/exists.xml +++ b/tests/fixtures/plan/exists.xml @@ -28,6 +28,7 @@ Content-Type: application/xml; charset=utf-8 days 10 2011-10-03T22:23:12Z + false diff --git a/tests/fixtures/subscription/show.xml b/tests/fixtures/subscription/show.xml index c7ef1e30..ea84641c 100644 --- a/tests/fixtures/subscription/show.xml +++ b/tests/fixtures/subscription/show.xml @@ -32,6 +32,7 @@ Content-Type: application/xml; charset=utf-8 0 usst + plan_free_trial usage From 510d2e9f91a8c7f9cbb2a11d938b49955f9944c1 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Wed, 19 Apr 2017 16:18:46 -0500 Subject: [PATCH 3/5] Remove parsing of X-Records header --- recurly/resource.py | 35 ++++++++++++++++++++++++---------- tests/fixtures/pages/count.xml | 9 +++++++++ tests/test_resources.py | 9 +++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 tests/fixtures/pages/count.xml diff --git a/recurly/resource.py b/recurly/resource.py index ca7fb519..b1e18a7f 100644 --- a/recurly/resource.py +++ b/recurly/resource.py @@ -95,15 +95,6 @@ def __iter__(self): pass raise StopIteration - def __len__(self): - try: - if not self.record_size: - return 0 - else: - return int(self.record_size) - except AttributeError: - return 0 - def next_page(self): """Return the next `Page` after this one in the result sequence it's from. @@ -142,6 +133,12 @@ def page_for_url(cls, url): return cls.page_for_value(resp, value) + @classmethod + def count_for_url(cls, url): + """Return the count of server side resources given a url""" + headers = Resource.headers_for_url(url) + return int(headers['X-Records']) + @classmethod def page_for_value(cls, resp, value): """Return a new `Page` representing the given resource `value` @@ -153,7 +150,6 @@ def page_for_value(cls, resp, value): """ page = cls(value) - page.record_size = resp.getheader('X-Records') links = parse_link_value(resp.getheader('Link')) for url, data in six.iteritems(links): if data.get('rel') == 'start': @@ -346,6 +342,15 @@ def get(cls, uuid): resp, elem = cls.element_for_url(url) return cls.from_element(elem) + @classmethod + def headers_for_url(cls, url): + """Return the headers only for the given URL as a dict""" + response = cls.http_request(url, method='HEAD') + if response.status != 200: + cls.raise_http_error(response) + + return Resource.headers_as_dict(response) + @classmethod def element_for_url(cls, url): """Return the resource at the given URL, as a @@ -606,6 +611,16 @@ def all(cls, **kwargs): url = '%s?%s' % (url, urlencode(kwargs)) return Page.page_for_url(url) + @classmethod + def count(cls, **kwargs): + """Return a count of server side resources given + filtering arguments in kwargs. + """ + url = urljoin(recurly.base_uri(), cls.collection_path) + if kwargs: + url = '%s?%s' % (url, urlencode(kwargs)) + return Page.count_for_url(url) + def save(self): """Save this `Resource` instance to the service. diff --git a/tests/fixtures/pages/count.xml b/tests/fixtures/pages/count.xml new file mode 100644 index 00000000..141e24d9 --- /dev/null +++ b/tests/fixtures/pages/count.xml @@ -0,0 +1,9 @@ +HEAD https://api.recurly.com/v2/accounts?begin_time=2017-05-01T10%3A30%3A01-06%3A00 HTTP/1.1 +X-Api-Version: {api-version} +Accept: application/xml +Authorization: Basic YXBpa2V5Og== +User-Agent: {user-agent} + + +HTTP/1.1 200 OK +X-Records: 23 diff --git a/tests/test_resources.py b/tests/test_resources.py index 5d1d0b3d..4ee6bb2b 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -774,6 +774,15 @@ def test_build_invoice(self): with self.mock_request('invoice/account-deleted.xml'): account.delete() + def test_count(self): + try: + with self.mock_request('pages/count.xml'): + num_accounts = Account.count(begin_time='2017-05-01T10:30:01-06:00') + + self.assertTrue(num_accounts, 23) + finally: + pass + def test_pages(self): account_code = 'pages-%s-%%d' % self.test_id all_test_accounts = list() From 3a7b9766431673ebf8b90e85512f5b5d0704c352 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Thu, 18 May 2017 13:47:09 -0500 Subject: [PATCH 4/5] temporarily force parsing of element as boolean --- recurly/resource.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/recurly/resource.py b/recurly/resource.py index b1e18a7f..7acbbbb7 100644 --- a/recurly/resource.py +++ b/recurly/resource.py @@ -405,6 +405,10 @@ def value_for_element(cls, elem): attr_type = elem.attrib.get('type') log.debug("Converting %r element with type %r", elem.tag, attr_type) + # TODO this trial_requires_billing_info check can be removed when + # the server starts sending the correct type + if elem.tag == 'trial_requires_billing_info': + return elem.text.strip() == 'true' if attr_type == 'integer': return int(elem.text.strip()) if attr_type == 'float': From 9e96f188b61fbe11adfc0e41407295a9c8fc1ad0 Mon Sep 17 00:00:00 2001 From: Benjamin Eckel Date: Fri, 19 May 2017 15:07:06 -0500 Subject: [PATCH 5/5] Bump 2.5.0 --- CHANGELOG | 14 ++++++++++++++ recurly/__init__.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 20692f31..bfad3202 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,19 @@ ## Unreleased +## Version 2.5.0 April 17, 2017 + +- Remove parsing of X-Records header +- Cardless Free Trial changes for 2.6 + +### Upgrade Notes + +This release will upgrade us to API version 2.6. There are two breaking changes: + +1. Since the X-Records header was removed in the pagination endpoint, you can no longer call `len()` on a Page and expect it to return a cached response. +From now on you need to explicitly call the `count()` class method on a Page. See [PR #202](https://github.com/recurly/recurly-client-python/pull/202) for more information. +2. For `POST /v2/subscriptions` Sending `None` for `total_billing_cycles` attribute will now override plan `total_billing_cycles` setting and will make subscription renew forever. +Omitting the attribute will cause the setting to default to the value of plan `total_billing_cycles`. + ## Version 2.4.4 March 23, 2017 - Add API version 2.5 changes diff --git a/recurly/__init__.py b/recurly/__init__.py index c6b5377e..fa0d305f 100644 --- a/recurly/__init__.py +++ b/recurly/__init__.py @@ -21,7 +21,7 @@ """ -__version__ = '2.4.4' +__version__ = '2.5.0' __python_version__ = '.'.join(map(str, sys.version_info[:3])) cached_rate_limits = {