Merge lp:~ricardokirkner/click-toolbelt/split-store-api-namespace-take-2 into lp:click-toolbelt

Proposed by Ricardo Kirkner
Status: Merged
Approved by: Ricardo Kirkner
Approved revision: 53
Merged at revision: 50
Proposed branch: lp:~ricardokirkner/click-toolbelt/split-store-api-namespace-take-2
Merge into: lp:click-toolbelt
Diff against target: 960 lines (+246/-221)
24 files modified
Makefile (+1/-1)
click_toolbelt/channels.py (+1/-1)
click_toolbelt/common.py (+3/-69)
click_toolbelt/info.py (+1/-1)
click_toolbelt/login.py (+1/-1)
click_toolbelt/tests/test_common.py (+2/-66)
click_toolbelt/tests/test_login.py (+1/-1)
click_toolbelt/tests/test_upload.py (+3/-3)
click_toolbelt/toolbelt.py (+1/-1)
click_toolbelt/upload.py (+1/-1)
setup.py (+1/-1)
storeapi/_login.py (+2/-3)
storeapi/_upload.py (+6/-6)
storeapi/channels.py (+3/-3)
storeapi/common.py (+71/-7)
storeapi/compat.py (+12/-0)
storeapi/constants.py (+10/-0)
storeapi/info.py (+1/-1)
storeapi/tests/test_channels.py (+8/-8)
storeapi/tests/test_common.py (+77/-17)
storeapi/tests/test_info.py (+3/-3)
storeapi/tests/test_login.py (+13/-11)
storeapi/tests/test_upload.py (+15/-16)
tests.py (+9/-0)
To merge this branch: bzr merge lp:~ricardokirkner/click-toolbelt/split-store-api-namespace-take-2
Reviewer Review Type Date Requested Status
Matias Bordese (community) Approve
Fabián Ezequiel Gallina (community) Approve
Review via email: mp+282001@code.launchpad.net

Commit message

split store api into standalone namespace for easier vendoring

To post a comment you must log in.
Revision history for this message
Fabián Ezequiel Gallina (fgallina) wrote :

LGTM

review: Approve
Revision history for this message
Matias Bordese (matiasb) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2014-02-24 12:43:21 +0000
3+++ Makefile 2016-01-08 14:21:43 +0000
4@@ -22,5 +22,5 @@
5 coverage:
6 @coverage erase
7 @coverage run --branch setup.py test
8- @coverage report --include='click_toolbelt/*' -m
9+ @coverage report --include='click_toolbelt/*,storeapi/*' -m
10
11
12=== modified file 'click_toolbelt/channels.py'
13--- click_toolbelt/channels.py 2015-12-22 15:28:53 +0000
14+++ click_toolbelt/channels.py 2016-01-08 14:21:43 +0000
15@@ -4,11 +4,11 @@
16 import json
17 import logging
18
19-from click_toolbelt.api.channels import get_channels, update_channels
20 from click_toolbelt.common import (
21 Command,
22 CommandError,
23 )
24+from storeapi.channels import get_channels, update_channels
25
26
27 class Channels(Command):
28
29=== modified file 'click_toolbelt/common.py'
30--- click_toolbelt/common.py 2015-12-21 18:41:57 +0000
31+++ click_toolbelt/common.py 2016-01-08 14:21:43 +0000
32@@ -1,12 +1,11 @@
33 # Copyright 2015 Canonical Ltd. This software is licensed under the
34 # GNU General Public License version 3 (see the file LICENSE).
35 from __future__ import absolute_import, unicode_literals
36-import time
37-from functools import wraps
38
39 import cliff.command
40
41 from click_toolbelt.config import clear_config, load_config, save_config
42+from storeapi.common import get_oauth_session
43
44
45 class CommandError(Exception):
46@@ -29,73 +28,8 @@
47
48 def get_oauth_session(self):
49 """Return a client configured to allow oauth signed requests."""
50- # import here to avoid circular import
51- from click_toolbelt.api.common import get_oauth_session
52- return get_oauth_session()
53+ config = load_config()
54+ return get_oauth_session(config)
55
56 def take_action(self, parsed_args):
57 pass # pragma: no cover
58-
59-
60-def is_scan_completed(response):
61- """Return True if the response indicates the scan process completed."""
62- if response.ok:
63- return response.json().get('completed', False)
64- return False
65-
66-
67-def retry(terminator=None, retries=3, delay=3, backoff=2, logger=None):
68- """Decorate a function to automatically retry calling it on failure.
69-
70- Arguments:
71- - terminator: this should be a callable that returns a boolean;
72- it is used to determine if the function call was successful
73- and the retry loop should be stopped
74- - retries: an integer specifying the maximum number of retries
75- - delay: initial number of seconds to wait for the first retry
76- - backoff: exponential factor to use to adapt the delay between
77- subsequent retries
78- - logger: logging.Logger instance to use for logging
79-
80- The decorated function will return as soon as any of the following
81- conditions are met:
82-
83- 1. terminator evaluates function output as True
84- 2. there are no more retries left
85-
86- If the terminator callable is not provided, the function will be called
87- exactly once and will not be retried.
88-
89- """
90- def decorated(func):
91- if retries != int(retries) or retries < 0:
92- raise ValueError(
93- 'retries value must be a positive integer or zero')
94- if delay < 0:
95- raise ValueError('delay value must be positive')
96-
97- if backoff != int(backoff) or backoff < 1:
98- raise ValueError('backoff value must be a positive integer')
99-
100- @wraps(func)
101- def wrapped(*args, **kwargs):
102- retries_left, current_delay = retries, delay
103-
104- result = func(*args, **kwargs)
105- if terminator is not None:
106- while not terminator(result) and retries_left > 0:
107- msg = "... retrying in %d seconds" % current_delay
108- if logger:
109- logger.warning(msg)
110-
111- # sleep
112- time.sleep(current_delay)
113- retries_left -= 1
114- current_delay *= backoff
115-
116- # retry
117- result = func(*args, **kwargs)
118- return result, retries_left == 0
119-
120- return wrapped
121- return decorated
122
123=== modified file 'click_toolbelt/info.py'
124--- click_toolbelt/info.py 2015-12-09 12:59:24 +0000
125+++ click_toolbelt/info.py 2016-01-08 14:21:43 +0000
126@@ -6,8 +6,8 @@
127
128 from cliff.command import Command
129
130-from click_toolbelt.api.info import get_info
131 from click_toolbelt.common import CommandError
132+from storeapi.info import get_info
133
134
135 class Info(Command):
136
137=== modified file 'click_toolbelt/login.py'
138--- click_toolbelt/login.py 2015-12-21 18:41:57 +0000
139+++ click_toolbelt/login.py 2016-01-08 14:21:43 +0000
140@@ -4,7 +4,6 @@
141 from __future__ import absolute_import, unicode_literals
142 import logging
143
144-from click_toolbelt.api import login
145 from click_toolbelt.common import (
146 Command,
147 CommandError,
148@@ -12,6 +11,7 @@
149 from click_toolbelt.constants import (
150 CLICK_TOOLBELT_PROJECT_NAME,
151 )
152+from storeapi import login
153
154
155 class Login(Command):
156
157=== modified file 'click_toolbelt/tests/test_common.py'
158--- click_toolbelt/tests/test_common.py 2015-12-21 18:41:57 +0000
159+++ click_toolbelt/tests/test_common.py 2016-01-08 14:21:43 +0000
160@@ -1,13 +1,11 @@
161 # Copyright 2015 Canonical Ltd. This software is licensed under the
162 # GNU General Public License version 3 (see the file LICENSE).
163 from __future__ import absolute_import, unicode_literals
164-from unittest import TestCase
165
166-from mock import Mock, call, patch
167+from mock import patch
168
169 from click_toolbelt.common import (
170 Command,
171- retry,
172 )
173 from click_toolbelt.tests.test_config import ConfigTestCase
174
175@@ -21,7 +19,7 @@
176 args = None
177 self.command = self.command_class(app, args)
178
179- patcher = patch('click_toolbelt.api.common.get_oauth_session')
180+ patcher = patch('click_toolbelt.common.get_oauth_session')
181 self.mock_get_oauth_session = patcher.start()
182 self.addCleanup(patcher.stop)
183
184@@ -44,65 +42,3 @@
185 def test_proxy_clear_config(self, mock_clear_config):
186 self.command.clear_config()
187 mock_clear_config.assert_called_once_with()
188-
189-
190-class RetryDecoratorTestCase(TestCase):
191-
192- def target(self, *args, **kwargs):
193- return dict(args=args, kwargs=kwargs)
194-
195- def test_retry(self):
196- result, aborted = retry()(self.target)()
197- self.assertEqual(result, dict(args=(), kwargs={}))
198- self.assertEqual(aborted, False)
199-
200- @patch('click_toolbelt.common.time.sleep')
201- def test_retry_small_backoff(self, mock_sleep):
202- mock_terminator = Mock()
203- mock_terminator.return_value = False
204-
205- delay = 0.001
206- result, aborted = retry(mock_terminator, retries=2,
207- delay=delay)(self.target)()
208-
209- self.assertEqual(result, dict(args=(), kwargs={}))
210- self.assertEqual(aborted, True)
211- self.assertEqual(mock_terminator.call_count, 3)
212- self.assertEqual(mock_sleep.mock_calls, [
213- call(delay),
214- call(delay * 2),
215- ])
216-
217- def test_retry_abort(self):
218- mock_terminator = Mock()
219- mock_terminator.return_value = False
220- mock_logger = Mock()
221-
222- result, aborted = retry(mock_terminator, delay=0.001, backoff=1,
223- logger=mock_logger)(self.target)()
224-
225- self.assertEqual(result, dict(args=(), kwargs={}))
226- self.assertEqual(aborted, True)
227- self.assertEqual(mock_terminator.call_count, 4)
228- self.assertEqual(mock_logger.warning.call_count, 3)
229-
230- def test_retry_with_invalid_retries(self):
231- for value in (0.1, -1):
232- with self.assertRaises(ValueError) as ctx:
233- retry(retries=value)(self.target)
234- self.assertEqual(
235- str(ctx.exception),
236- 'retries value must be a positive integer or zero')
237-
238- def test_retry_with_negative_delay(self):
239- with self.assertRaises(ValueError) as ctx:
240- retry(delay=-1)(self.target)
241- self.assertEqual(str(ctx.exception),
242- 'delay value must be positive')
243-
244- def test_retry_with_invalid_backoff(self):
245- for value in (-1, 0, 0.1):
246- with self.assertRaises(ValueError) as ctx:
247- retry(backoff=value)(self.target)
248- self.assertEqual(str(ctx.exception),
249- 'backoff value must be a positive integer')
250
251=== modified file 'click_toolbelt/tests/test_login.py'
252--- click_toolbelt/tests/test_login.py 2015-12-22 15:28:53 +0000
253+++ click_toolbelt/tests/test_login.py 2016-01-08 14:21:43 +0000
254@@ -37,7 +37,7 @@
255 mock_environ = {
256 'UBUNTU_SSO_API_ROOT_URL': UBUNTU_SSO_API_ROOT_URL,
257 }
258- patcher = patch('click_toolbelt.api._login.os.environ', mock_environ)
259+ patcher = patch('storeapi._login.os.environ', mock_environ)
260 patcher.start()
261 self.addCleanup(patcher.stop)
262
263
264=== modified file 'click_toolbelt/tests/test_upload.py'
265--- click_toolbelt/tests/test_upload.py 2015-12-21 18:41:57 +0000
266+++ click_toolbelt/tests/test_upload.py 2016-01-08 14:21:43 +0000
267@@ -25,13 +25,13 @@
268 self.mock_get = self.mock_get_oauth_session.return_value.get
269 self.mock_post = self.mock_get_oauth_session.return_value.post
270
271- p = patch('click_toolbelt.api._upload.logger')
272+ p = patch('storeapi._upload.logger')
273 self.mock_logger = p.start()
274 self.addCleanup(p.stop)
275- p = patch('click_toolbelt.api._upload.upload_files')
276+ p = patch('storeapi._upload.upload_files')
277 self.mock_upload_files = p.start()
278 self.addCleanup(p.stop)
279- p = patch('click_toolbelt.api._upload.upload_app')
280+ p = patch('storeapi._upload.upload_app')
281 self.mock_upload_app = p.start()
282 self.addCleanup(p.stop)
283
284
285=== modified file 'click_toolbelt/toolbelt.py'
286--- click_toolbelt/toolbelt.py 2015-12-21 18:09:53 +0000
287+++ click_toolbelt/toolbelt.py 2016-01-08 14:21:43 +0000
288@@ -1,6 +1,6 @@
289+#!/usr/bin/env python
290 # Copyright 2013 Canonical Ltd. This software is licensed under the
291 # GNU General Public License version 3 (see the file LICENSE).
292-#!/usr/bin/env python
293 from __future__ import absolute_import, unicode_literals
294 import sys
295
296
297=== modified file 'click_toolbelt/upload.py'
298--- click_toolbelt/upload.py 2015-12-21 18:41:57 +0000
299+++ click_toolbelt/upload.py 2016-01-08 14:21:43 +0000
300@@ -3,11 +3,11 @@
301 from __future__ import absolute_import, unicode_literals
302 import logging
303
304-from click_toolbelt.api import upload
305 from click_toolbelt.common import (
306 Command,
307 CommandError,
308 )
309+from storeapi import upload
310
311
312 class Upload(Command):
313
314=== modified file 'setup.py'
315--- setup.py 2015-12-09 19:32:37 +0000
316+++ setup.py 2016-01-08 14:21:43 +0000
317@@ -67,7 +67,7 @@
318
319 zip_safe=False,
320
321- test_suite='click_toolbelt.tests',
322+ test_suite='tests',
323 tests_require=[
324 'mock',
325 'responses',
326
327=== renamed directory 'click_toolbelt/api' => 'storeapi'
328=== modified file 'storeapi/_login.py'
329--- click_toolbelt/api/_login.py 2015-12-21 18:41:57 +0000
330+++ storeapi/_login.py 2016-01-08 14:21:43 +0000
331@@ -10,13 +10,12 @@
332 V2ApiClient,
333 )
334
335-from click_toolbelt.constants import (
336- CLICK_TOOLBELT_PROJECT_NAME,
337+from storeapi.constants import (
338 UBUNTU_SSO_API_ROOT_URL,
339 )
340
341
342-def login(email, password, otp=None, token_name=CLICK_TOOLBELT_PROJECT_NAME):
343+def login(email, password, token_name, otp=None):
344 """Log in via the Ubuntu One SSO API.
345
346 If successful, returns the oauth token data.
347
348=== modified file 'storeapi/_upload.py'
349--- click_toolbelt/api/_upload.py 2015-12-21 18:41:57 +0000
350+++ storeapi/_upload.py 2016-01-08 14:21:43 +0000
351@@ -6,13 +6,13 @@
352 import os
353 import re
354
355-from click_toolbelt.api.common import get_oauth_session
356-from click_toolbelt.common import (
357+from storeapi.common import (
358+ get_oauth_session,
359 is_scan_completed,
360 retry,
361 )
362-from click_toolbelt.compat import open, quote_plus, urljoin
363-from click_toolbelt.constants import (
364+from storeapi.compat import open, quote_plus, urljoin
365+from storeapi.constants import (
366 CLICK_UPDOWN_UPLOAD_URL,
367 MYAPPS_API_ROOT_URL,
368 SCAN_STATUS_POLL_DELAY,
369@@ -61,11 +61,11 @@
370
371 if errors:
372 logger.info('Some errors were detected:\n\n%s\n\n',
373- '\n'.join(errors))
374+ '\n'.join(errors))
375
376 if app_url:
377 logger.info('Please check out the application at: %s.\n',
378- app_url)
379+ app_url)
380
381 return success
382
383
384=== modified file 'storeapi/channels.py'
385--- click_toolbelt/api/channels.py 2015-12-09 19:10:19 +0000
386+++ storeapi/channels.py 2016-01-08 14:21:43 +0000
387@@ -3,7 +3,7 @@
388 # GNU General Public License version 3 (see the file LICENSE).
389 from __future__ import absolute_import, unicode_literals
390
391-from click_toolbelt.api.common import myapps_api_call
392+from storeapi.common import myapps_api_call
393
394
395 def get_channels(session, package_name):
396@@ -15,8 +15,8 @@
397 def update_channels(session, package_name, data):
398 """Update current channels config for package through API."""
399 channels_endpoint = 'package-channels/%s/' % package_name
400- result = myapps_api_call(channels_endpoint, method='POST',
401- data=data, session=session)
402+ result = myapps_api_call(channels_endpoint, method='POST',
403+ data=data, session=session)
404 if result['success']:
405 result['errors'] = result['data']['errors']
406 result['data'] = result['data']['channels']
407
408=== modified file 'storeapi/common.py'
409--- click_toolbelt/api/common.py 2015-12-11 20:03:40 +0000
410+++ storeapi/common.py 2016-01-08 14:21:43 +0000
411@@ -2,18 +2,18 @@
412 # GNU General Public License version 3 (see the file LICENSE).
413 import json
414 import os
415+import time
416+from functools import wraps
417
418 import requests
419 from requests_oauthlib import OAuth1Session
420
421-from click_toolbelt.compat import urljoin
422-from click_toolbelt.config import load_config
423-from click_toolbelt.constants import MYAPPS_API_ROOT_URL
424-
425-
426-def get_oauth_session():
427+from storeapi.compat import urljoin
428+from storeapi.constants import MYAPPS_API_ROOT_URL
429+
430+
431+def get_oauth_session(config):
432 """Return a client configured to allow oauth signed requests."""
433- config = load_config()
434 try:
435 session = OAuth1Session(
436 config['consumer_key'],
437@@ -51,3 +51,67 @@
438 else:
439 result['errors'] = [response.text]
440 return result
441+
442+
443+def is_scan_completed(response):
444+ """Return True if the response indicates the scan process completed."""
445+ if response.ok:
446+ return response.json().get('completed', False)
447+ return False
448+
449+
450+def retry(terminator=None, retries=3, delay=3, backoff=2, logger=None):
451+ """Decorate a function to automatically retry calling it on failure.
452+
453+ Arguments:
454+ - terminator: this should be a callable that returns a boolean;
455+ it is used to determine if the function call was successful
456+ and the retry loop should be stopped
457+ - retries: an integer specifying the maximum number of retries
458+ - delay: initial number of seconds to wait for the first retry
459+ - backoff: exponential factor to use to adapt the delay between
460+ subsequent retries
461+ - logger: logging.Logger instance to use for logging
462+
463+ The decorated function will return as soon as any of the following
464+ conditions are met:
465+
466+ 1. terminator evaluates function output as True
467+ 2. there are no more retries left
468+
469+ If the terminator callable is not provided, the function will be called
470+ exactly once and will not be retried.
471+
472+ """
473+ def decorated(func):
474+ if retries != int(retries) or retries < 0:
475+ raise ValueError(
476+ 'retries value must be a positive integer or zero')
477+ if delay < 0:
478+ raise ValueError('delay value must be positive')
479+
480+ if backoff != int(backoff) or backoff < 1:
481+ raise ValueError('backoff value must be a positive integer')
482+
483+ @wraps(func)
484+ def wrapped(*args, **kwargs):
485+ retries_left, current_delay = retries, delay
486+
487+ result = func(*args, **kwargs)
488+ if terminator is not None:
489+ while not terminator(result) and retries_left > 0:
490+ msg = "... retrying in %d seconds" % current_delay
491+ if logger:
492+ logger.warning(msg)
493+
494+ # sleep
495+ time.sleep(current_delay)
496+ retries_left -= 1
497+ current_delay *= backoff
498+
499+ # retry
500+ result = func(*args, **kwargs)
501+ return result, retries_left == 0
502+
503+ return wrapped
504+ return decorated
505
506=== added file 'storeapi/compat.py'
507--- storeapi/compat.py 1970-01-01 00:00:00 +0000
508+++ storeapi/compat.py 2016-01-08 14:21:43 +0000
509@@ -0,0 +1,12 @@
510+# Copyright 2015 Canonical Ltd. This software is licensed under the
511+# GNU General Public License version 3 (see the file LICENSE).
512+from __future__ import absolute_import, unicode_literals
513+
514+
515+try: # pragma: no cover
516+ from builtins import open # noqa
517+ from urllib.parse import quote_plus, urljoin
518+except ImportError: # pragma: no cover
519+ from __builtin__ import open # noqa
520+ from urllib import quote_plus # noqa
521+ from urlparse import urljoin # noqa
522
523=== added file 'storeapi/constants.py'
524--- storeapi/constants.py 1970-01-01 00:00:00 +0000
525+++ storeapi/constants.py 2016-01-08 14:21:43 +0000
526@@ -0,0 +1,10 @@
527+# Copyright 2015 Canonical Ltd. This software is licensed under the
528+# GNU General Public License version 3 (see the file LICENSE).
529+from __future__ import absolute_import, unicode_literals
530+
531+
532+CLICK_UPDOWN_UPLOAD_URL = 'https://upload.apps.ubuntu.com/'
533+MYAPPS_API_ROOT_URL = 'https://myapps.developer.ubuntu.com/dev/api/'
534+UBUNTU_SSO_API_ROOT_URL = 'https://login.ubuntu.com/api/v2/'
535+SCAN_STATUS_POLL_DELAY = 5
536+SCAN_STATUS_POLL_RETRIES = 5
537
538=== modified file 'storeapi/info.py'
539--- click_toolbelt/api/info.py 2015-12-09 19:10:19 +0000
540+++ storeapi/info.py 2016-01-08 14:21:43 +0000
541@@ -3,7 +3,7 @@
542 # GNU General Public License version 3 (see the file LICENSE).
543 from __future__ import absolute_import, unicode_literals
544
545-from click_toolbelt.api.common import myapps_api_call
546+from storeapi.common import myapps_api_call
547
548
549 def get_info():
550
551=== renamed directory 'click_toolbelt/tests/api' => 'storeapi/tests'
552=== modified file 'storeapi/tests/test_channels.py'
553--- click_toolbelt/tests/api/test_channels.py 2015-12-21 16:05:53 +0000
554+++ storeapi/tests/test_channels.py 2016-01-08 14:21:43 +0000
555@@ -3,20 +3,20 @@
556 # GNU General Public License version 3 (see the file LICENSE).
557 from __future__ import absolute_import, unicode_literals
558 import json
559+from unittest import TestCase
560
561 from mock import patch
562
563-from click_toolbelt.api.channels import get_channels, update_channels
564-from click_toolbelt.tests.test_config import ConfigTestCase
565-
566-
567-class ChannelsAPITestCase(ConfigTestCase):
568+from storeapi.channels import get_channels, update_channels
569+
570+
571+class ChannelsAPITestCase(TestCase):
572
573 def setUp(self):
574 super(ChannelsAPITestCase, self).setUp()
575
576 # setup patches
577- oauth_session = 'click_toolbelt.api.common.get_oauth_session'
578+ oauth_session = 'storeapi.common.get_oauth_session'
579 patcher = patch(oauth_session)
580 self.mock_get_oauth_session = patcher.start()
581 self.mock_session = self.mock_get_oauth_session.return_value
582@@ -87,7 +87,7 @@
583 self.assertEqual(data, expected)
584
585 def test_get_channels_uses_environment_variables(self):
586- with patch('click_toolbelt.api.common.os.environ',
587+ with patch('storeapi.common.os.environ',
588 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):
589 get_channels(self.mock_session, 'package.name')
590 self.mock_get.assert_called_once_with(
591@@ -135,7 +135,7 @@
592 self.assertEqual(data, expected)
593
594 def test_update_channels_uses_environment_variables(self):
595- with patch('click_toolbelt.api.common.os.environ',
596+ with patch('storeapi.common.os.environ',
597 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):
598 update_channels(
599 self.mock_session, 'package.name', {'stable': 2})
600
601=== modified file 'storeapi/tests/test_common.py'
602--- click_toolbelt/tests/api/test_common.py 2015-12-11 20:03:40 +0000
603+++ storeapi/tests/test_common.py 2016-01-08 14:21:43 +0000
604@@ -4,42 +4,39 @@
605 from unittest import TestCase
606
607 import responses
608-from mock import Mock, patch
609+from mock import Mock, call, patch
610 from requests_oauthlib import OAuth1Session
611
612-from click_toolbelt.api.common import get_oauth_session, myapps_api_call
613+from storeapi.common import (
614+ get_oauth_session,
615+ myapps_api_call,
616+ retry,
617+)
618
619
620 class GetOAuthSessionTestCase(TestCase):
621
622- def setUp(self):
623- super(GetOAuthSessionTestCase, self).setUp()
624- patcher = patch(
625- 'click_toolbelt.api.common.load_config')
626- self.mock_load_config = patcher.start()
627- self.addCleanup(patcher.stop)
628-
629 def test_get_oauth_session_when_no_config(self):
630- self.mock_load_config.return_value = {}
631- session = get_oauth_session()
632+ config = {}
633+ session = get_oauth_session(config)
634 self.assertIsNone(session)
635
636 def test_get_oauth_session_when_partial_config(self):
637- self.mock_load_config.return_value = {
638+ config = {
639 'consumer_key': 'consumer-key',
640 'consumer_secret': 'consumer-secret',
641 }
642- session = get_oauth_session()
643+ session = get_oauth_session(config)
644 self.assertIsNone(session)
645
646 def test_get_oauth_session(self):
647- self.mock_load_config.return_value = {
648+ config = {
649 'consumer_key': 'consumer-key',
650 'consumer_secret': 'consumer-secret',
651 'token_key': 'token-key',
652 'token_secret': 'token-secret',
653 }
654- session = get_oauth_session()
655+ session = get_oauth_session(config)
656 self.assertIsInstance(session, OAuth1Session)
657 self.assertEqual(session.auth.client.client_key, 'consumer-key')
658 self.assertEqual(session.auth.client.client_secret, 'consumer-secret')
659@@ -52,7 +49,7 @@
660
661 def setUp(self):
662 super(ApiCallTestCase, self).setUp()
663- p = patch('click_toolbelt.api.common.os')
664+ p = patch('storeapi.common.os')
665 mock_os = p.start()
666 self.addCleanup(p.stop)
667 mock_os.environ = {'MYAPPS_API_ROOT_URL': 'http://example.com'}
668@@ -130,7 +127,8 @@
669 responses.add(responses.POST, 'http://example.com/path',
670 json=response_data)
671
672- result = myapps_api_call('/path', method='POST', data={'request': 'value'})
673+ result = myapps_api_call(
674+ '/path', method='POST', data={'request': 'value'})
675 self.assertEqual(result, {
676 'success': True,
677 'data': response_data,
678@@ -141,3 +139,65 @@
679 'application/json')
680 self.assertEqual(responses.calls[0].request.body,
681 json.dumps({'request': 'value'}))
682+
683+
684+class RetryDecoratorTestCase(TestCase):
685+
686+ def target(self, *args, **kwargs):
687+ return dict(args=args, kwargs=kwargs)
688+
689+ def test_retry(self):
690+ result, aborted = retry()(self.target)()
691+ self.assertEqual(result, dict(args=(), kwargs={}))
692+ self.assertEqual(aborted, False)
693+
694+ @patch('storeapi.common.time.sleep')
695+ def test_retry_small_backoff(self, mock_sleep):
696+ mock_terminator = Mock()
697+ mock_terminator.return_value = False
698+
699+ delay = 0.001
700+ result, aborted = retry(mock_terminator, retries=2,
701+ delay=delay)(self.target)()
702+
703+ self.assertEqual(result, dict(args=(), kwargs={}))
704+ self.assertEqual(aborted, True)
705+ self.assertEqual(mock_terminator.call_count, 3)
706+ self.assertEqual(mock_sleep.mock_calls, [
707+ call(delay),
708+ call(delay * 2),
709+ ])
710+
711+ def test_retry_abort(self):
712+ mock_terminator = Mock()
713+ mock_terminator.return_value = False
714+ mock_logger = Mock()
715+
716+ result, aborted = retry(mock_terminator, delay=0.001, backoff=1,
717+ logger=mock_logger)(self.target)()
718+
719+ self.assertEqual(result, dict(args=(), kwargs={}))
720+ self.assertEqual(aborted, True)
721+ self.assertEqual(mock_terminator.call_count, 4)
722+ self.assertEqual(mock_logger.warning.call_count, 3)
723+
724+ def test_retry_with_invalid_retries(self):
725+ for value in (0.1, -1):
726+ with self.assertRaises(ValueError) as ctx:
727+ retry(retries=value)(self.target)
728+ self.assertEqual(
729+ str(ctx.exception),
730+ 'retries value must be a positive integer or zero')
731+
732+ def test_retry_with_negative_delay(self):
733+ with self.assertRaises(ValueError) as ctx:
734+ retry(delay=-1)(self.target)
735+ self.assertEqual(str(ctx.exception),
736+ 'delay value must be positive')
737+
738+ def test_retry_with_invalid_backoff(self):
739+ for value in (-1, 0, 0.1):
740+ with self.assertRaises(ValueError) as ctx:
741+ retry(backoff=value)(self.target)
742+ self.assertEqual(str(ctx.exception),
743+ 'backoff value must be a positive integer')
744
745=== modified file 'storeapi/tests/test_info.py'
746--- click_toolbelt/tests/api/test_info.py 2015-12-09 19:10:19 +0000
747+++ storeapi/tests/test_info.py 2016-01-08 14:21:43 +0000
748@@ -6,7 +6,7 @@
749
750 from mock import patch
751
752-from click_toolbelt.api.info import get_info
753+from storeapi.info import get_info
754
755
756 class InfoAPITestCase(TestCase):
757@@ -14,7 +14,7 @@
758 def setUp(self):
759 super(InfoAPITestCase, self).setUp()
760
761- patcher = patch('click_toolbelt.api.common.requests.get')
762+ patcher = patch('storeapi.common.requests.get')
763 self.mock_get = patcher.start()
764 self.mock_response = self.mock_get.return_value
765 self.addCleanup(patcher.stop)
766@@ -42,7 +42,7 @@
767 self.assertEqual(data, expected)
768
769 def test_get_info_uses_environment_variables(self):
770- with patch('click_toolbelt.api.common.os.environ',
771+ with patch('storeapi.common.os.environ',
772 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):
773 get_info()
774 self.mock_get.assert_called_once_with('http://example.com')
775
776=== modified file 'storeapi/tests/test_login.py'
777--- click_toolbelt/tests/api/test_login.py 2015-12-21 18:41:57 +0000
778+++ storeapi/tests/test_login.py 2016-01-08 14:21:43 +0000
779@@ -8,9 +8,8 @@
780 from mock import patch
781 from requests import Response
782
783-from click_toolbelt.api._login import login
784-from click_toolbelt.constants import (
785- CLICK_TOOLBELT_PROJECT_NAME,
786+from storeapi._login import login
787+from storeapi.constants import (
788 UBUNTU_SSO_API_ROOT_URL,
789 )
790
791@@ -21,12 +20,13 @@
792 super(LoginAPITestCase, self).setUp()
793 self.email = 'user@domain.com'
794 self.password = 'password'
795+ self.token_name = 'token-name'
796
797 # setup patches
798 mock_environ = {
799 'UBUNTU_SSO_API_ROOT_URL': UBUNTU_SSO_API_ROOT_URL,
800 }
801- patcher = patch('click_toolbelt.api._login.os.environ', mock_environ)
802+ patcher = patch('storeapi._login.os.environ', mock_environ)
803 patcher.start()
804 self.addCleanup(patcher.stop)
805
806@@ -51,8 +51,9 @@
807 response._content = json.dumps(data).encode('utf-8')
808 return response
809
810- def assert_login_request(self, otp=None,
811- token_name=CLICK_TOOLBELT_PROJECT_NAME):
812+ def assert_login_request(self, otp=None, token_name=None):
813+ if token_name is None:
814+ token_name = self.token_name
815 data = {
816 'email': self.email,
817 'password': self.password,
818@@ -67,12 +68,12 @@
819 )
820
821 def test_login_successful(self):
822- result = login(self.email, self.password)
823+ result = login(self.email, self.password, self.token_name)
824 expected = {'success': True, 'body': self.token_data}
825 self.assertEqual(result, expected)
826
827 def test_default_token_name(self):
828- result = login(self.email, self.password)
829+ result = login(self.email, self.password, self.token_name)
830 expected = {'success': True, 'body': self.token_data}
831 self.assertEqual(result, expected)
832 self.assert_login_request()
833@@ -84,7 +85,8 @@
834 self.assert_login_request(token_name='my-token')
835
836 def test_login_with_otp(self):
837- result = login(self.email, self.password, otp='123456')
838+ result = login(self.email, self.password, self.token_name,
839+ otp='123456')
840 expected = {'success': True, 'body': self.token_data}
841 self.assertEqual(result, expected)
842 self.assert_login_request(otp='123456')
843@@ -99,7 +101,7 @@
844 status_code=401, reason='UNAUTHORISED', data=error_data)
845 self.mock_request.return_value = response
846
847- result = login(self.email, self.password)
848+ result = login(self.email, self.password, self.token_name)
849 expected = {'success': False, 'body': error_data}
850 self.assertEqual(result, expected)
851
852@@ -113,6 +115,6 @@
853 status_code=401, reason='UNAUTHORISED', data=error_data)
854 self.mock_request.return_value = response
855
856- result = login(self.email, self.password)
857+ result = login(self.email, self.password, self.token_name)
858 expected = {'success': False, 'body': error_data}
859 self.assertEqual(result, expected)
860
861=== modified file 'storeapi/tests/test_upload.py'
862--- click_toolbelt/tests/api/test_upload.py 2015-12-21 18:41:57 +0000
863+++ storeapi/tests/test_upload.py 2016-01-08 14:21:43 +0000
864@@ -4,28 +4,26 @@
865 import json
866 import os
867 import tempfile
868+from unittest import TestCase
869
870 from mock import ANY, patch
871 from requests import Response
872
873-from click_toolbelt.api._upload import (
874+from storeapi._upload import (
875 get_upload_url,
876 upload_app,
877 upload_files,
878 upload,
879 )
880-from click_toolbelt.tests.test_config import (
881- ConfigTestCase,
882-)
883-
884-
885-class UploadBaseTestCase(ConfigTestCase):
886+
887+
888+class UploadBaseTestCase(TestCase):
889
890 def setUp(self):
891 super(UploadBaseTestCase, self).setUp()
892
893 # setup patches
894- name = 'click_toolbelt.api._upload.get_oauth_session'
895+ name = 'storeapi._upload.get_oauth_session'
896 patcher = patch(name)
897 self.mock_get_oauth_session = patcher.start()
898 self.addCleanup(patcher.stop)
899@@ -33,14 +31,15 @@
900 self.mock_get = self.mock_get_oauth_session.return_value.get
901 self.mock_post = self.mock_get_oauth_session.return_value.post
902
903-
904-class UploadWithScanTestCase(UploadBaseTestCase):
905-
906- def setUp(self):
907- super(UploadWithScanTestCase, self).setUp()
908 self.suffix = '_0.1_all.click'
909 self.binary_file = self.get_temporary_file(suffix=self.suffix)
910
911+ def get_temporary_file(self, suffix='.cfg'):
912+ return tempfile.NamedTemporaryFile(suffix=suffix)
913+
914+
915+class UploadWithScanTestCase(UploadBaseTestCase):
916+
917 def test_default_metadata(self):
918 mock_response = self.mock_post.return_value
919 mock_response.ok = True
920@@ -201,7 +200,7 @@
921 self.package_name = 'namespace.binary'
922
923 patcher = patch.multiple(
924- 'click_toolbelt.api._upload',
925+ 'storeapi._upload',
926 SCAN_STATUS_POLL_DELAY=0.0001)
927 patcher.start()
928 self.addCleanup(patcher.stop)
929@@ -450,7 +449,7 @@
930 files=[],
931 )
932
933- @patch('click_toolbelt.api._upload.open')
934+ @patch('storeapi._upload.open')
935 def test_upload_app_with_icon(self, mock_open):
936 with tempfile.NamedTemporaryFile() as icon:
937 mock_open.return_value = icon
938@@ -473,7 +472,7 @@
939 ],
940 )
941
942- @patch('click_toolbelt.api._upload.open')
943+ @patch('storeapi._upload.open')
944 def test_upload_app_with_screenshots(self, mock_open):
945 screenshot1 = tempfile.NamedTemporaryFile()
946 screenshot2 = tempfile.NamedTemporaryFile()
947
948=== added file 'tests.py'
949--- tests.py 1970-01-01 00:00:00 +0000
950+++ tests.py 2016-01-08 14:21:43 +0000
951@@ -0,0 +1,9 @@
952+from unittest import TestLoader, TestSuite
953+
954+
955+def load_tests(loader, tests, pattern):
956+ suites = [
957+ TestLoader().discover('click_toolbelt', top_level_dir='.'),
958+ TestLoader().discover('storeapi', top_level_dir='.'),
959+ ]
960+ return TestSuite(suites)

Subscribers

People subscribed via source and target branches