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()