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 541921a9..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 = { @@ -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 @@ -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/recurly/resource.py b/recurly/resource.py index ca7fb519..7acbbbb7 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 @@ -400,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': @@ -606,6 +615,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/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 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()