Skip to content

Commit c577c88

Browse files
committed
[0.2.0] Pickle compatible objects + python 2 fix + pagination format
1 parent 9d367ad commit c577c88

File tree

6 files changed

+104
-40
lines changed

6 files changed

+104
-40
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to this project will be documented here.
44

5+
## [0.2.0] - 2019-07-01
6+
### Added
7+
- Pickle compatible objects
8+
### Fixed
9+
- Pagination next link format.
10+
- Python 2 compatibility.
11+
512
## [0.1.0] - 2019-06-28
613
### Added
714
- Client to query Help Scout's v2 API.

helpscout/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from helpscout.client import HelpScout # noqa
22

33

4-
__version__ = '0.1.0'
4+
__version__ = '0.2.0'

helpscout/client.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import time
33

44
from functools import partial
5-
from urllib.parse import urljoin
5+
try: # Python 3
6+
from urllib.parse import urljoin
7+
except ImportError: # Python 2
8+
from urlparse import urljoin
69

710
import requests
811

@@ -136,13 +139,16 @@ def hit(self, endpoint, method, resource_id=None, data=None):
136139
yield
137140
elif ok:
138141
response = r.json()
139-
yield from self._results_with_pagination(response, method)
142+
for item in self._results_with_pagination(response, method):
143+
yield item
140144
elif status_code == 401:
141145
self._authenticate()
142-
yield from self.hit(endpoint, method, resource_id, data)
146+
for item in self.hit(endpoint, method, resource_id, data):
147+
yield item
143148
elif status_code == 429:
144149
self._handle_rate_limit_exceeded()
145-
yield from self.hit(endpoint, method, resource_id, data)
150+
for item in self.hit(endpoint, method, resource_id, data):
151+
yield item
146152
else:
147153
raise HelpScoutException(r.text)
148154

@@ -165,21 +171,25 @@ def _results_with_pagination(self, response, method):
165171
yield response
166172
return
167173
if isinstance(response[EmbeddedKey], list):
168-
yield from response[EmbeddedKey]
174+
for item in response[EmbeddedKey]:
175+
yield item
169176
else:
170177
yield response[EmbeddedKey]
171-
next_page = response.get('_links', {}).get('next')
178+
next_obj = response.get('_links', {}).get('next', {})
179+
next_page = None if next_obj is None else next_obj.get('href')
172180
while next_page:
173181
headers = self._authentication_headers()
174182
logger.debug('%s %s' % (method, next_page))
175183
r = getattr(requests, method)(next_page, headers=headers)
176184
if r.ok:
177185
response = r.json()
178186
if isinstance(response[EmbeddedKey], list):
179-
yield from response[EmbeddedKey]
187+
for item in response[EmbeddedKey]:
188+
yield item
180189
else:
181190
yield response[EmbeddedKey]
182-
next_page = response.get('_links', {}).get('next')
191+
next_obj = response.get('_links', {}).get('next', {})
192+
next_page = None if next_obj is None else next_obj.get('href')
183193
elif r.status_code == 401:
184194
self._authenticate()
185195
elif r.status_code == 429:

helpscout/exceptions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ class HelpScoutException(Exception):
55
class HelpScoutAuthenticationException(HelpScoutException):
66
def __init__(self, *args):
77
text = ' '.join(str(arg) for arg in args)
8-
super().__init__('HelpScout authentication failed: ' + text)
8+
super(HelpScoutAuthenticationException, self).__init__(
9+
'HelpScout authentication failed: ' + text)
910

1011

1112
class HelpScoutRateLimitExceededException(HelpScoutException):

helpscout/model.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
class HelpScoutObject:
1+
class HelpScoutObject(object):
22

33
key = ''
44

@@ -60,16 +60,31 @@ def cls(cls, entity_name, key):
6060
-------
6161
type: The object's class
6262
"""
63-
existing_class = classes.get(entity_name)
64-
if existing_class is not None:
65-
return existing_class
6663
plural_letters = (-2 if entity_name.endswith('es') else
6764
-1 if entity_name.endswith('s') else
6865
None)
6966
class_name = entity_name.capitalize()[:plural_letters]
70-
classes[entity_name] = cls = type(class_name, (cls,), {'key': key})
67+
existing_class = globals().get(class_name)
68+
if existing_class is not None:
69+
return existing_class
70+
globals()[class_name] = cls = type(class_name, (cls,), {'key': key})
7171
return cls
7272

73+
def __reduce__(self):
74+
"""For pickling with HelpScoutObject."""
75+
class_attributes = self.__class__.__name__, self.key
76+
return get_subclass_instance, class_attributes, self.__getstate__()
77+
78+
def __getstate__(self):
79+
"""Pickle dump implementation."""
80+
return self._attrs, tuple(getattr(self, attr) for attr in self._attrs)
81+
82+
def __setstate__(self, state):
83+
"""Pickle load implementation."""
84+
self._attrs = state[0]
85+
for attr, value in zip(self._attrs, state[1]):
86+
setattr(self, attr, value)
87+
7388
def __eq__(self, other):
7489
"""Equality comparison."""
7590
if self.__class__ is not other.__class__:
@@ -107,4 +122,20 @@ def __repr__(self):
107122
__str__ = __repr__
108123

109124

110-
classes = {}
125+
def get_subclass_instance(class_name, key):
126+
"""Gets a dynamic class from a class name for unpickling.
127+
128+
Parameters
129+
----------
130+
name: str
131+
A class name, expected to start with Upper case.
132+
133+
Returns
134+
-------
135+
A helpscout object subclass.
136+
"""
137+
cls = globals().get(class_name)
138+
if cls is None:
139+
cls = type(class_name, (HelpScoutObject,), {'key': key})
140+
globals()[class_name] = cls
141+
return cls.__new__(cls)

tests/helpscout/test_client.py

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -345,14 +345,15 @@ def test_pagination_embedded_next_page_ok(self):
345345
{'msg': 'hello'},
346346
{'msg': 'bye'},
347347
],
348-
'_links': {'next': 'http://helpscout.com/next_page/110'}
348+
'_links': {'next': {'href': 'http://helpscout.com/next_page/110'}}
349349
}
350350
responses_values = [
351351
{EmbeddedKey: [
352352
{'msg': 'blink 1'},
353353
{'msg': 'blink 2'},
354354
],
355-
'_links': {'next': 'http://helpscout.com/next_page/111'}},
355+
'_links': {'next':
356+
{'href': 'http://helpscout.com/next_page/111'}}},
356357
{EmbeddedKey: [
357358
{'msg': 'see ya'},
358359
],
@@ -384,12 +385,15 @@ def test_pagination_embedded_next_page_ok(self):
384385
self.assertEqual(auth_headers.call_count, 2)
385386
self.assertEqual(
386387
logger.debug.call_args_list,
387-
[call(method + ' ' + response_value['_links']['next']),
388-
call(method + ' ' + responses_values[0]['_links']['next'])])
388+
[call(method + ' ' + response_value['_links']['next']['href']),
389+
call(method + ' ' + responses_values[0]['_links']['next'][
390+
'href'])])
389391
self.assertEqual(
390392
requests.get.call_args_list,
391-
[call(response_value['_links']['next'], headers=headers),
392-
call(responses_values[0]['_links']['next'], headers=headers)
393+
[call(response_value['_links']['next']['href'],
394+
headers=headers),
395+
call(responses_values[0]['_links']['next']['href'],
396+
headers=headers)
393397
])
394398
responses[0].json.assert_called_once()
395399
responses[1].json.assert_called_once()
@@ -403,14 +407,15 @@ def test_pagination_embedded_next_page_token_expired(self):
403407
{'msg': 'hello'},
404408
{'msg': 'bye'},
405409
],
406-
'_links': {'next': 'http://helpscout.com/next_page/110'}
410+
'_links': {'next': {'href': 'http://helpscout.com/next_page/110'}}
407411
}
408412
responses_values = [
409413
{EmbeddedKey: [
410414
{'msg': 'blink 1'},
411415
{'msg': 'blink 2'},
412416
],
413-
'_links': {'next': 'http://helpscout.com/next_page/111'}},
417+
'_links': {'next':
418+
{'href': 'http://helpscout.com/next_page/111'}}},
414419
{EmbeddedKey: [
415420
{'msg': 'see ya'},
416421
],
@@ -444,14 +449,18 @@ def test_pagination_embedded_next_page_token_expired(self):
444449
self.assertEqual(auth_headers.call_count, 3)
445450
self.assertEqual(
446451
logger.debug.call_args_list,
447-
[call(method + ' ' + response_value['_links']['next']),
448-
call(method + ' ' + response_value['_links']['next']),
449-
call(method + ' ' + responses_values[0]['_links']['next'])])
452+
[call(method + ' ' + response_value['_links']['next']['href']),
453+
call(method + ' ' + response_value['_links']['next']['href']),
454+
call(method + ' ' + responses_values[0]['_links']['next'][
455+
'href'])])
450456
self.assertEqual(
451457
requests.get.call_args_list,
452-
[call(response_value['_links']['next'], headers=headers),
453-
call(response_value['_links']['next'], headers=headers),
454-
call(responses_values[0]['_links']['next'], headers=headers)
458+
[call(response_value['_links']['next']['href'],
459+
headers=headers),
460+
call(response_value['_links']['next']['href'],
461+
headers=headers),
462+
call(responses_values[0]['_links']['next']['href'],
463+
headers=headers)
455464
])
456465
responses[1].json.assert_called_once()
457466
responses[2].json.assert_called_once()
@@ -465,14 +474,15 @@ def test_pagination_embedded_next_page_rate_limit_exceeded(self):
465474
{'msg': 'hello'},
466475
{'msg': 'bye'},
467476
],
468-
'_links': {'next': 'http://helpscout.com/next_page/110'}
477+
'_links': {'next': {'href': 'http://helpscout.com/next_page/110'}}
469478
}
470479
responses_values = [
471480
{EmbeddedKey: [
472481
{'msg': 'blink 1'},
473482
{'msg': 'blink 2'},
474483
],
475-
'_links': {'next': 'http://helpscout.com/next_page/111'}},
484+
'_links': {'next':
485+
{'href': 'http://helpscout.com/next_page/111'}}},
476486
{EmbeddedKey: [
477487
{'msg': 'see ya'},
478488
],
@@ -506,14 +516,18 @@ def test_pagination_embedded_next_page_rate_limit_exceeded(self):
506516
self.assertEqual(auth_headers.call_count, 3)
507517
self.assertEqual(
508518
logger.debug.call_args_list,
509-
[call(method + ' ' + response_value['_links']['next']),
510-
call(method + ' ' + response_value['_links']['next']),
511-
call(method + ' ' + responses_values[0]['_links']['next'])])
519+
[call(method + ' ' + response_value['_links']['next']['href']),
520+
call(method + ' ' + response_value['_links']['next']['href']),
521+
call(method + ' ' + responses_values[0]['_links']['next'][
522+
'href'])])
512523
self.assertEqual(
513524
requests.get.call_args_list,
514-
[call(response_value['_links']['next'], headers=headers),
515-
call(response_value['_links']['next'], headers=headers),
516-
call(responses_values[0]['_links']['next'], headers=headers)
525+
[call(response_value['_links']['next']['href'],
526+
headers=headers),
527+
call(response_value['_links']['next']['href'],
528+
headers=headers),
529+
call(responses_values[0]['_links']['next']['href'],
530+
headers=headers)
517531
])
518532
responses[0].json.assert_not_called()
519533
responses[1].json.assert_called_once()
@@ -528,14 +542,15 @@ def test_pagination_exception(self):
528542
{'msg': 'hello'},
529543
{'msg': 'bye'},
530544
],
531-
'_links': {'next': 'http://helpscout.com/next_page/110'}
545+
'_links': {'next': {'href': 'http://helpscout.com/next_page/110'}}
532546
}
533547
responses_values = [
534548
{EmbeddedKey: [
535549
{'msg': 'blink 1'},
536550
{'msg': 'blink 2'},
537551
],
538-
'_links': {'next': 'http://helpscout.com/next_page/111'}},
552+
'_links': {'next':
553+
{'href': 'http://helpscout.com/next_page/111'}}},
539554
{EmbeddedKey: [
540555
{'msg': 'see ya'},
541556
],

0 commit comments

Comments
 (0)