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
=== modified file 'Makefile'
--- Makefile 2014-02-24 12:43:21 +0000
+++ Makefile 2016-01-08 14:21:43 +0000
@@ -22,5 +22,5 @@
22coverage:22coverage:
23 @coverage erase23 @coverage erase
24 @coverage run --branch setup.py test24 @coverage run --branch setup.py test
25 @coverage report --include='click_toolbelt/*' -m25 @coverage report --include='click_toolbelt/*,storeapi/*' -m
2626
2727
=== modified file 'click_toolbelt/channels.py'
--- click_toolbelt/channels.py 2015-12-22 15:28:53 +0000
+++ click_toolbelt/channels.py 2016-01-08 14:21:43 +0000
@@ -4,11 +4,11 @@
4import json4import json
5import logging5import logging
66
7from click_toolbelt.api.channels import get_channels, update_channels
8from click_toolbelt.common import (7from click_toolbelt.common import (
9 Command,8 Command,
10 CommandError,9 CommandError,
11)10)
11from storeapi.channels import get_channels, update_channels
1212
1313
14class Channels(Command):14class Channels(Command):
1515
=== modified file 'click_toolbelt/common.py'
--- click_toolbelt/common.py 2015-12-21 18:41:57 +0000
+++ click_toolbelt/common.py 2016-01-08 14:21:43 +0000
@@ -1,12 +1,11 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU General Public License version 3 (see the file LICENSE).2# GNU General Public License version 3 (see the file LICENSE).
3from __future__ import absolute_import, unicode_literals3from __future__ import absolute_import, unicode_literals
4import time
5from functools import wraps
64
7import cliff.command5import cliff.command
86
9from click_toolbelt.config import clear_config, load_config, save_config7from click_toolbelt.config import clear_config, load_config, save_config
8from storeapi.common import get_oauth_session
109
1110
12class CommandError(Exception):11class CommandError(Exception):
@@ -29,73 +28,8 @@
2928
30 def get_oauth_session(self):29 def get_oauth_session(self):
31 """Return a client configured to allow oauth signed requests."""30 """Return a client configured to allow oauth signed requests."""
32 # import here to avoid circular import31 config = load_config()
33 from click_toolbelt.api.common import get_oauth_session32 return get_oauth_session(config)
34 return get_oauth_session()
3533
36 def take_action(self, parsed_args):34 def take_action(self, parsed_args):
37 pass # pragma: no cover35 pass # pragma: no cover
38
39
40def is_scan_completed(response):
41 """Return True if the response indicates the scan process completed."""
42 if response.ok:
43 return response.json().get('completed', False)
44 return False
45
46
47def retry(terminator=None, retries=3, delay=3, backoff=2, logger=None):
48 """Decorate a function to automatically retry calling it on failure.
49
50 Arguments:
51 - terminator: this should be a callable that returns a boolean;
52 it is used to determine if the function call was successful
53 and the retry loop should be stopped
54 - retries: an integer specifying the maximum number of retries
55 - delay: initial number of seconds to wait for the first retry
56 - backoff: exponential factor to use to adapt the delay between
57 subsequent retries
58 - logger: logging.Logger instance to use for logging
59
60 The decorated function will return as soon as any of the following
61 conditions are met:
62
63 1. terminator evaluates function output as True
64 2. there are no more retries left
65
66 If the terminator callable is not provided, the function will be called
67 exactly once and will not be retried.
68
69 """
70 def decorated(func):
71 if retries != int(retries) or retries < 0:
72 raise ValueError(
73 'retries value must be a positive integer or zero')
74 if delay < 0:
75 raise ValueError('delay value must be positive')
76
77 if backoff != int(backoff) or backoff < 1:
78 raise ValueError('backoff value must be a positive integer')
79
80 @wraps(func)
81 def wrapped(*args, **kwargs):
82 retries_left, current_delay = retries, delay
83
84 result = func(*args, **kwargs)
85 if terminator is not None:
86 while not terminator(result) and retries_left > 0:
87 msg = "... retrying in %d seconds" % current_delay
88 if logger:
89 logger.warning(msg)
90
91 # sleep
92 time.sleep(current_delay)
93 retries_left -= 1
94 current_delay *= backoff
95
96 # retry
97 result = func(*args, **kwargs)
98 return result, retries_left == 0
99
100 return wrapped
101 return decorated
10236
=== modified file 'click_toolbelt/info.py'
--- click_toolbelt/info.py 2015-12-09 12:59:24 +0000
+++ click_toolbelt/info.py 2016-01-08 14:21:43 +0000
@@ -6,8 +6,8 @@
66
7from cliff.command import Command7from cliff.command import Command
88
9from click_toolbelt.api.info import get_info
10from click_toolbelt.common import CommandError9from click_toolbelt.common import CommandError
10from storeapi.info import get_info
1111
1212
13class Info(Command):13class Info(Command):
1414
=== modified file 'click_toolbelt/login.py'
--- click_toolbelt/login.py 2015-12-21 18:41:57 +0000
+++ click_toolbelt/login.py 2016-01-08 14:21:43 +0000
@@ -4,7 +4,6 @@
4from __future__ import absolute_import, unicode_literals4from __future__ import absolute_import, unicode_literals
5import logging5import logging
66
7from click_toolbelt.api import login
8from click_toolbelt.common import (7from click_toolbelt.common import (
9 Command,8 Command,
10 CommandError,9 CommandError,
@@ -12,6 +11,7 @@
12from click_toolbelt.constants import (11from click_toolbelt.constants import (
13 CLICK_TOOLBELT_PROJECT_NAME,12 CLICK_TOOLBELT_PROJECT_NAME,
14)13)
14from storeapi import login
1515
1616
17class Login(Command):17class Login(Command):
1818
=== modified file 'click_toolbelt/tests/test_common.py'
--- click_toolbelt/tests/test_common.py 2015-12-21 18:41:57 +0000
+++ click_toolbelt/tests/test_common.py 2016-01-08 14:21:43 +0000
@@ -1,13 +1,11 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU General Public License version 3 (see the file LICENSE).2# GNU General Public License version 3 (see the file LICENSE).
3from __future__ import absolute_import, unicode_literals3from __future__ import absolute_import, unicode_literals
4from unittest import TestCase
54
6from mock import Mock, call, patch5from mock import patch
76
8from click_toolbelt.common import (7from click_toolbelt.common import (
9 Command,8 Command,
10 retry,
11)9)
12from click_toolbelt.tests.test_config import ConfigTestCase10from click_toolbelt.tests.test_config import ConfigTestCase
1311
@@ -21,7 +19,7 @@
21 args = None19 args = None
22 self.command = self.command_class(app, args)20 self.command = self.command_class(app, args)
2321
24 patcher = patch('click_toolbelt.api.common.get_oauth_session')22 patcher = patch('click_toolbelt.common.get_oauth_session')
25 self.mock_get_oauth_session = patcher.start()23 self.mock_get_oauth_session = patcher.start()
26 self.addCleanup(patcher.stop)24 self.addCleanup(patcher.stop)
2725
@@ -44,65 +42,3 @@
44 def test_proxy_clear_config(self, mock_clear_config):42 def test_proxy_clear_config(self, mock_clear_config):
45 self.command.clear_config()43 self.command.clear_config()
46 mock_clear_config.assert_called_once_with()44 mock_clear_config.assert_called_once_with()
47
48
49class RetryDecoratorTestCase(TestCase):
50
51 def target(self, *args, **kwargs):
52 return dict(args=args, kwargs=kwargs)
53
54 def test_retry(self):
55 result, aborted = retry()(self.target)()
56 self.assertEqual(result, dict(args=(), kwargs={}))
57 self.assertEqual(aborted, False)
58
59 @patch('click_toolbelt.common.time.sleep')
60 def test_retry_small_backoff(self, mock_sleep):
61 mock_terminator = Mock()
62 mock_terminator.return_value = False
63
64 delay = 0.001
65 result, aborted = retry(mock_terminator, retries=2,
66 delay=delay)(self.target)()
67
68 self.assertEqual(result, dict(args=(), kwargs={}))
69 self.assertEqual(aborted, True)
70 self.assertEqual(mock_terminator.call_count, 3)
71 self.assertEqual(mock_sleep.mock_calls, [
72 call(delay),
73 call(delay * 2),
74 ])
75
76 def test_retry_abort(self):
77 mock_terminator = Mock()
78 mock_terminator.return_value = False
79 mock_logger = Mock()
80
81 result, aborted = retry(mock_terminator, delay=0.001, backoff=1,
82 logger=mock_logger)(self.target)()
83
84 self.assertEqual(result, dict(args=(), kwargs={}))
85 self.assertEqual(aborted, True)
86 self.assertEqual(mock_terminator.call_count, 4)
87 self.assertEqual(mock_logger.warning.call_count, 3)
88
89 def test_retry_with_invalid_retries(self):
90 for value in (0.1, -1):
91 with self.assertRaises(ValueError) as ctx:
92 retry(retries=value)(self.target)
93 self.assertEqual(
94 str(ctx.exception),
95 'retries value must be a positive integer or zero')
96
97 def test_retry_with_negative_delay(self):
98 with self.assertRaises(ValueError) as ctx:
99 retry(delay=-1)(self.target)
100 self.assertEqual(str(ctx.exception),
101 'delay value must be positive')
102
103 def test_retry_with_invalid_backoff(self):
104 for value in (-1, 0, 0.1):
105 with self.assertRaises(ValueError) as ctx:
106 retry(backoff=value)(self.target)
107 self.assertEqual(str(ctx.exception),
108 'backoff value must be a positive integer')
10945
=== modified file 'click_toolbelt/tests/test_login.py'
--- click_toolbelt/tests/test_login.py 2015-12-22 15:28:53 +0000
+++ click_toolbelt/tests/test_login.py 2016-01-08 14:21:43 +0000
@@ -37,7 +37,7 @@
37 mock_environ = {37 mock_environ = {
38 'UBUNTU_SSO_API_ROOT_URL': UBUNTU_SSO_API_ROOT_URL,38 'UBUNTU_SSO_API_ROOT_URL': UBUNTU_SSO_API_ROOT_URL,
39 }39 }
40 patcher = patch('click_toolbelt.api._login.os.environ', mock_environ)40 patcher = patch('storeapi._login.os.environ', mock_environ)
41 patcher.start()41 patcher.start()
42 self.addCleanup(patcher.stop)42 self.addCleanup(patcher.stop)
4343
4444
=== modified file 'click_toolbelt/tests/test_upload.py'
--- click_toolbelt/tests/test_upload.py 2015-12-21 18:41:57 +0000
+++ click_toolbelt/tests/test_upload.py 2016-01-08 14:21:43 +0000
@@ -25,13 +25,13 @@
25 self.mock_get = self.mock_get_oauth_session.return_value.get25 self.mock_get = self.mock_get_oauth_session.return_value.get
26 self.mock_post = self.mock_get_oauth_session.return_value.post26 self.mock_post = self.mock_get_oauth_session.return_value.post
2727
28 p = patch('click_toolbelt.api._upload.logger')28 p = patch('storeapi._upload.logger')
29 self.mock_logger = p.start()29 self.mock_logger = p.start()
30 self.addCleanup(p.stop)30 self.addCleanup(p.stop)
31 p = patch('click_toolbelt.api._upload.upload_files')31 p = patch('storeapi._upload.upload_files')
32 self.mock_upload_files = p.start()32 self.mock_upload_files = p.start()
33 self.addCleanup(p.stop)33 self.addCleanup(p.stop)
34 p = patch('click_toolbelt.api._upload.upload_app')34 p = patch('storeapi._upload.upload_app')
35 self.mock_upload_app = p.start()35 self.mock_upload_app = p.start()
36 self.addCleanup(p.stop)36 self.addCleanup(p.stop)
3737
3838
=== modified file 'click_toolbelt/toolbelt.py'
--- click_toolbelt/toolbelt.py 2015-12-21 18:09:53 +0000
+++ click_toolbelt/toolbelt.py 2016-01-08 14:21:43 +0000
@@ -1,6 +1,6 @@
1#!/usr/bin/env python
1# Copyright 2013 Canonical Ltd. This software is licensed under the2# Copyright 2013 Canonical Ltd. This software is licensed under the
2# GNU General Public License version 3 (see the file LICENSE).3# GNU General Public License version 3 (see the file LICENSE).
3#!/usr/bin/env python
4from __future__ import absolute_import, unicode_literals4from __future__ import absolute_import, unicode_literals
5import sys5import sys
66
77
=== modified file 'click_toolbelt/upload.py'
--- click_toolbelt/upload.py 2015-12-21 18:41:57 +0000
+++ click_toolbelt/upload.py 2016-01-08 14:21:43 +0000
@@ -3,11 +3,11 @@
3from __future__ import absolute_import, unicode_literals3from __future__ import absolute_import, unicode_literals
4import logging4import logging
55
6from click_toolbelt.api import upload
7from click_toolbelt.common import (6from click_toolbelt.common import (
8 Command,7 Command,
9 CommandError,8 CommandError,
10)9)
10from storeapi import upload
1111
1212
13class Upload(Command):13class Upload(Command):
1414
=== modified file 'setup.py'
--- setup.py 2015-12-09 19:32:37 +0000
+++ setup.py 2016-01-08 14:21:43 +0000
@@ -67,7 +67,7 @@
6767
68 zip_safe=False,68 zip_safe=False,
6969
70 test_suite='click_toolbelt.tests',70 test_suite='tests',
71 tests_require=[71 tests_require=[
72 'mock',72 'mock',
73 'responses',73 'responses',
7474
=== renamed directory 'click_toolbelt/api' => 'storeapi'
=== modified file 'storeapi/_login.py'
--- click_toolbelt/api/_login.py 2015-12-21 18:41:57 +0000
+++ storeapi/_login.py 2016-01-08 14:21:43 +0000
@@ -10,13 +10,12 @@
10 V2ApiClient,10 V2ApiClient,
11)11)
1212
13from click_toolbelt.constants import (13from storeapi.constants import (
14 CLICK_TOOLBELT_PROJECT_NAME,
15 UBUNTU_SSO_API_ROOT_URL,14 UBUNTU_SSO_API_ROOT_URL,
16)15)
1716
1817
19def login(email, password, otp=None, token_name=CLICK_TOOLBELT_PROJECT_NAME):18def login(email, password, token_name, otp=None):
20 """Log in via the Ubuntu One SSO API.19 """Log in via the Ubuntu One SSO API.
2120
22 If successful, returns the oauth token data.21 If successful, returns the oauth token data.
2322
=== modified file 'storeapi/_upload.py'
--- click_toolbelt/api/_upload.py 2015-12-21 18:41:57 +0000
+++ storeapi/_upload.py 2016-01-08 14:21:43 +0000
@@ -6,13 +6,13 @@
6import os6import os
7import re7import re
88
9from click_toolbelt.api.common import get_oauth_session9from storeapi.common import (
10from click_toolbelt.common import (10 get_oauth_session,
11 is_scan_completed,11 is_scan_completed,
12 retry,12 retry,
13)13)
14from click_toolbelt.compat import open, quote_plus, urljoin14from storeapi.compat import open, quote_plus, urljoin
15from click_toolbelt.constants import (15from storeapi.constants import (
16 CLICK_UPDOWN_UPLOAD_URL,16 CLICK_UPDOWN_UPLOAD_URL,
17 MYAPPS_API_ROOT_URL,17 MYAPPS_API_ROOT_URL,
18 SCAN_STATUS_POLL_DELAY,18 SCAN_STATUS_POLL_DELAY,
@@ -61,11 +61,11 @@
6161
62 if errors:62 if errors:
63 logger.info('Some errors were detected:\n\n%s\n\n',63 logger.info('Some errors were detected:\n\n%s\n\n',
64 '\n'.join(errors))64 '\n'.join(errors))
6565
66 if app_url:66 if app_url:
67 logger.info('Please check out the application at: %s.\n',67 logger.info('Please check out the application at: %s.\n',
68 app_url)68 app_url)
6969
70 return success70 return success
7171
7272
=== modified file 'storeapi/channels.py'
--- click_toolbelt/api/channels.py 2015-12-09 19:10:19 +0000
+++ storeapi/channels.py 2016-01-08 14:21:43 +0000
@@ -3,7 +3,7 @@
3# GNU General Public License version 3 (see the file LICENSE).3# GNU General Public License version 3 (see the file LICENSE).
4from __future__ import absolute_import, unicode_literals4from __future__ import absolute_import, unicode_literals
55
6from click_toolbelt.api.common import myapps_api_call6from storeapi.common import myapps_api_call
77
88
9def get_channels(session, package_name):9def get_channels(session, package_name):
@@ -15,8 +15,8 @@
15def update_channels(session, package_name, data):15def update_channels(session, package_name, data):
16 """Update current channels config for package through API."""16 """Update current channels config for package through API."""
17 channels_endpoint = 'package-channels/%s/' % package_name17 channels_endpoint = 'package-channels/%s/' % package_name
18 result = myapps_api_call(channels_endpoint, method='POST',18 result = myapps_api_call(channels_endpoint, method='POST',
19 data=data, session=session)19 data=data, session=session)
20 if result['success']:20 if result['success']:
21 result['errors'] = result['data']['errors']21 result['errors'] = result['data']['errors']
22 result['data'] = result['data']['channels']22 result['data'] = result['data']['channels']
2323
=== modified file 'storeapi/common.py'
--- click_toolbelt/api/common.py 2015-12-11 20:03:40 +0000
+++ storeapi/common.py 2016-01-08 14:21:43 +0000
@@ -2,18 +2,18 @@
2# GNU General Public License version 3 (see the file LICENSE).2# GNU General Public License version 3 (see the file LICENSE).
3import json3import json
4import os4import os
5import time
6from functools import wraps
57
6import requests8import requests
7from requests_oauthlib import OAuth1Session9from requests_oauthlib import OAuth1Session
810
9from click_toolbelt.compat import urljoin11from storeapi.compat import urljoin
10from click_toolbelt.config import load_config12from storeapi.constants import MYAPPS_API_ROOT_URL
11from click_toolbelt.constants import MYAPPS_API_ROOT_URL13
1214
1315def get_oauth_session(config):
14def get_oauth_session():
15 """Return a client configured to allow oauth signed requests."""16 """Return a client configured to allow oauth signed requests."""
16 config = load_config()
17 try:17 try:
18 session = OAuth1Session(18 session = OAuth1Session(
19 config['consumer_key'],19 config['consumer_key'],
@@ -51,3 +51,67 @@
51 else:51 else:
52 result['errors'] = [response.text]52 result['errors'] = [response.text]
53 return result53 return result
54
55
56def is_scan_completed(response):
57 """Return True if the response indicates the scan process completed."""
58 if response.ok:
59 return response.json().get('completed', False)
60 return False
61
62
63def retry(terminator=None, retries=3, delay=3, backoff=2, logger=None):
64 """Decorate a function to automatically retry calling it on failure.
65
66 Arguments:
67 - terminator: this should be a callable that returns a boolean;
68 it is used to determine if the function call was successful
69 and the retry loop should be stopped
70 - retries: an integer specifying the maximum number of retries
71 - delay: initial number of seconds to wait for the first retry
72 - backoff: exponential factor to use to adapt the delay between
73 subsequent retries
74 - logger: logging.Logger instance to use for logging
75
76 The decorated function will return as soon as any of the following
77 conditions are met:
78
79 1. terminator evaluates function output as True
80 2. there are no more retries left
81
82 If the terminator callable is not provided, the function will be called
83 exactly once and will not be retried.
84
85 """
86 def decorated(func):
87 if retries != int(retries) or retries < 0:
88 raise ValueError(
89 'retries value must be a positive integer or zero')
90 if delay < 0:
91 raise ValueError('delay value must be positive')
92
93 if backoff != int(backoff) or backoff < 1:
94 raise ValueError('backoff value must be a positive integer')
95
96 @wraps(func)
97 def wrapped(*args, **kwargs):
98 retries_left, current_delay = retries, delay
99
100 result = func(*args, **kwargs)
101 if terminator is not None:
102 while not terminator(result) and retries_left > 0:
103 msg = "... retrying in %d seconds" % current_delay
104 if logger:
105 logger.warning(msg)
106
107 # sleep
108 time.sleep(current_delay)
109 retries_left -= 1
110 current_delay *= backoff
111
112 # retry
113 result = func(*args, **kwargs)
114 return result, retries_left == 0
115
116 return wrapped
117 return decorated
54118
=== added file 'storeapi/compat.py'
--- storeapi/compat.py 1970-01-01 00:00:00 +0000
+++ storeapi/compat.py 2016-01-08 14:21:43 +0000
@@ -0,0 +1,12 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU General Public License version 3 (see the file LICENSE).
3from __future__ import absolute_import, unicode_literals
4
5
6try: # pragma: no cover
7 from builtins import open # noqa
8 from urllib.parse import quote_plus, urljoin
9except ImportError: # pragma: no cover
10 from __builtin__ import open # noqa
11 from urllib import quote_plus # noqa
12 from urlparse import urljoin # noqa
013
=== added file 'storeapi/constants.py'
--- storeapi/constants.py 1970-01-01 00:00:00 +0000
+++ storeapi/constants.py 2016-01-08 14:21:43 +0000
@@ -0,0 +1,10 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU General Public License version 3 (see the file LICENSE).
3from __future__ import absolute_import, unicode_literals
4
5
6CLICK_UPDOWN_UPLOAD_URL = 'https://upload.apps.ubuntu.com/'
7MYAPPS_API_ROOT_URL = 'https://myapps.developer.ubuntu.com/dev/api/'
8UBUNTU_SSO_API_ROOT_URL = 'https://login.ubuntu.com/api/v2/'
9SCAN_STATUS_POLL_DELAY = 5
10SCAN_STATUS_POLL_RETRIES = 5
011
=== modified file 'storeapi/info.py'
--- click_toolbelt/api/info.py 2015-12-09 19:10:19 +0000
+++ storeapi/info.py 2016-01-08 14:21:43 +0000
@@ -3,7 +3,7 @@
3# GNU General Public License version 3 (see the file LICENSE).3# GNU General Public License version 3 (see the file LICENSE).
4from __future__ import absolute_import, unicode_literals4from __future__ import absolute_import, unicode_literals
55
6from click_toolbelt.api.common import myapps_api_call6from storeapi.common import myapps_api_call
77
88
9def get_info():9def get_info():
1010
=== renamed directory 'click_toolbelt/tests/api' => 'storeapi/tests'
=== modified file 'storeapi/tests/test_channels.py'
--- click_toolbelt/tests/api/test_channels.py 2015-12-21 16:05:53 +0000
+++ storeapi/tests/test_channels.py 2016-01-08 14:21:43 +0000
@@ -3,20 +3,20 @@
3# GNU General Public License version 3 (see the file LICENSE).3# GNU General Public License version 3 (see the file LICENSE).
4from __future__ import absolute_import, unicode_literals4from __future__ import absolute_import, unicode_literals
5import json5import json
6from unittest import TestCase
67
7from mock import patch8from mock import patch
89
9from click_toolbelt.api.channels import get_channels, update_channels10from storeapi.channels import get_channels, update_channels
10from click_toolbelt.tests.test_config import ConfigTestCase11
1112
1213class ChannelsAPITestCase(TestCase):
13class ChannelsAPITestCase(ConfigTestCase):
1414
15 def setUp(self):15 def setUp(self):
16 super(ChannelsAPITestCase, self).setUp()16 super(ChannelsAPITestCase, self).setUp()
1717
18 # setup patches18 # setup patches
19 oauth_session = 'click_toolbelt.api.common.get_oauth_session'19 oauth_session = 'storeapi.common.get_oauth_session'
20 patcher = patch(oauth_session)20 patcher = patch(oauth_session)
21 self.mock_get_oauth_session = patcher.start()21 self.mock_get_oauth_session = patcher.start()
22 self.mock_session = self.mock_get_oauth_session.return_value22 self.mock_session = self.mock_get_oauth_session.return_value
@@ -87,7 +87,7 @@
87 self.assertEqual(data, expected)87 self.assertEqual(data, expected)
8888
89 def test_get_channels_uses_environment_variables(self):89 def test_get_channels_uses_environment_variables(self):
90 with patch('click_toolbelt.api.common.os.environ',90 with patch('storeapi.common.os.environ',
91 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):91 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):
92 get_channels(self.mock_session, 'package.name')92 get_channels(self.mock_session, 'package.name')
93 self.mock_get.assert_called_once_with(93 self.mock_get.assert_called_once_with(
@@ -135,7 +135,7 @@
135 self.assertEqual(data, expected)135 self.assertEqual(data, expected)
136136
137 def test_update_channels_uses_environment_variables(self):137 def test_update_channels_uses_environment_variables(self):
138 with patch('click_toolbelt.api.common.os.environ',138 with patch('storeapi.common.os.environ',
139 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):139 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):
140 update_channels(140 update_channels(
141 self.mock_session, 'package.name', {'stable': 2})141 self.mock_session, 'package.name', {'stable': 2})
142142
=== modified file 'storeapi/tests/test_common.py'
--- click_toolbelt/tests/api/test_common.py 2015-12-11 20:03:40 +0000
+++ storeapi/tests/test_common.py 2016-01-08 14:21:43 +0000
@@ -4,42 +4,39 @@
4from unittest import TestCase4from unittest import TestCase
55
6import responses6import responses
7from mock import Mock, patch7from mock import Mock, call, patch
8from requests_oauthlib import OAuth1Session8from requests_oauthlib import OAuth1Session
99
10from click_toolbelt.api.common import get_oauth_session, myapps_api_call10from storeapi.common import (
11 get_oauth_session,
12 myapps_api_call,
13 retry,
14)
1115
1216
13class GetOAuthSessionTestCase(TestCase):17class GetOAuthSessionTestCase(TestCase):
1418
15 def setUp(self):
16 super(GetOAuthSessionTestCase, self).setUp()
17 patcher = patch(
18 'click_toolbelt.api.common.load_config')
19 self.mock_load_config = patcher.start()
20 self.addCleanup(patcher.stop)
21
22 def test_get_oauth_session_when_no_config(self):19 def test_get_oauth_session_when_no_config(self):
23 self.mock_load_config.return_value = {}20 config = {}
24 session = get_oauth_session()21 session = get_oauth_session(config)
25 self.assertIsNone(session)22 self.assertIsNone(session)
2623
27 def test_get_oauth_session_when_partial_config(self):24 def test_get_oauth_session_when_partial_config(self):
28 self.mock_load_config.return_value = {25 config = {
29 'consumer_key': 'consumer-key',26 'consumer_key': 'consumer-key',
30 'consumer_secret': 'consumer-secret',27 'consumer_secret': 'consumer-secret',
31 }28 }
32 session = get_oauth_session()29 session = get_oauth_session(config)
33 self.assertIsNone(session)30 self.assertIsNone(session)
3431
35 def test_get_oauth_session(self):32 def test_get_oauth_session(self):
36 self.mock_load_config.return_value = {33 config = {
37 'consumer_key': 'consumer-key',34 'consumer_key': 'consumer-key',
38 'consumer_secret': 'consumer-secret',35 'consumer_secret': 'consumer-secret',
39 'token_key': 'token-key',36 'token_key': 'token-key',
40 'token_secret': 'token-secret',37 'token_secret': 'token-secret',
41 }38 }
42 session = get_oauth_session()39 session = get_oauth_session(config)
43 self.assertIsInstance(session, OAuth1Session)40 self.assertIsInstance(session, OAuth1Session)
44 self.assertEqual(session.auth.client.client_key, 'consumer-key')41 self.assertEqual(session.auth.client.client_key, 'consumer-key')
45 self.assertEqual(session.auth.client.client_secret, 'consumer-secret')42 self.assertEqual(session.auth.client.client_secret, 'consumer-secret')
@@ -52,7 +49,7 @@
5249
53 def setUp(self):50 def setUp(self):
54 super(ApiCallTestCase, self).setUp()51 super(ApiCallTestCase, self).setUp()
55 p = patch('click_toolbelt.api.common.os')52 p = patch('storeapi.common.os')
56 mock_os = p.start()53 mock_os = p.start()
57 self.addCleanup(p.stop)54 self.addCleanup(p.stop)
58 mock_os.environ = {'MYAPPS_API_ROOT_URL': 'http://example.com'}55 mock_os.environ = {'MYAPPS_API_ROOT_URL': 'http://example.com'}
@@ -130,7 +127,8 @@
130 responses.add(responses.POST, 'http://example.com/path',127 responses.add(responses.POST, 'http://example.com/path',
131 json=response_data)128 json=response_data)
132129
133 result = myapps_api_call('/path', method='POST', data={'request': 'value'})130 result = myapps_api_call(
131 '/path', method='POST', data={'request': 'value'})
134 self.assertEqual(result, {132 self.assertEqual(result, {
135 'success': True,133 'success': True,
136 'data': response_data,134 'data': response_data,
@@ -141,3 +139,65 @@
141 'application/json')139 'application/json')
142 self.assertEqual(responses.calls[0].request.body,140 self.assertEqual(responses.calls[0].request.body,
143 json.dumps({'request': 'value'}))141 json.dumps({'request': 'value'}))
142
143
144class RetryDecoratorTestCase(TestCase):
145
146 def target(self, *args, **kwargs):
147 return dict(args=args, kwargs=kwargs)
148
149 def test_retry(self):
150 result, aborted = retry()(self.target)()
151 self.assertEqual(result, dict(args=(), kwargs={}))
152 self.assertEqual(aborted, False)
153
154 @patch('storeapi.common.time.sleep')
155 def test_retry_small_backoff(self, mock_sleep):
156 mock_terminator = Mock()
157 mock_terminator.return_value = False
158
159 delay = 0.001
160 result, aborted = retry(mock_terminator, retries=2,
161 delay=delay)(self.target)()
162
163 self.assertEqual(result, dict(args=(), kwargs={}))
164 self.assertEqual(aborted, True)
165 self.assertEqual(mock_terminator.call_count, 3)
166 self.assertEqual(mock_sleep.mock_calls, [
167 call(delay),
168 call(delay * 2),
169 ])
170
171 def test_retry_abort(self):
172 mock_terminator = Mock()
173 mock_terminator.return_value = False
174 mock_logger = Mock()
175
176 result, aborted = retry(mock_terminator, delay=0.001, backoff=1,
177 logger=mock_logger)(self.target)()
178
179 self.assertEqual(result, dict(args=(), kwargs={}))
180 self.assertEqual(aborted, True)
181 self.assertEqual(mock_terminator.call_count, 4)
182 self.assertEqual(mock_logger.warning.call_count, 3)
183
184 def test_retry_with_invalid_retries(self):
185 for value in (0.1, -1):
186 with self.assertRaises(ValueError) as ctx:
187 retry(retries=value)(self.target)
188 self.assertEqual(
189 str(ctx.exception),
190 'retries value must be a positive integer or zero')
191
192 def test_retry_with_negative_delay(self):
193 with self.assertRaises(ValueError) as ctx:
194 retry(delay=-1)(self.target)
195 self.assertEqual(str(ctx.exception),
196 'delay value must be positive')
197
198 def test_retry_with_invalid_backoff(self):
199 for value in (-1, 0, 0.1):
200 with self.assertRaises(ValueError) as ctx:
201 retry(backoff=value)(self.target)
202 self.assertEqual(str(ctx.exception),
203 'backoff value must be a positive integer')
144204
=== modified file 'storeapi/tests/test_info.py'
--- click_toolbelt/tests/api/test_info.py 2015-12-09 19:10:19 +0000
+++ storeapi/tests/test_info.py 2016-01-08 14:21:43 +0000
@@ -6,7 +6,7 @@
66
7from mock import patch7from mock import patch
88
9from click_toolbelt.api.info import get_info9from storeapi.info import get_info
1010
1111
12class InfoAPITestCase(TestCase):12class InfoAPITestCase(TestCase):
@@ -14,7 +14,7 @@
14 def setUp(self):14 def setUp(self):
15 super(InfoAPITestCase, self).setUp()15 super(InfoAPITestCase, self).setUp()
1616
17 patcher = patch('click_toolbelt.api.common.requests.get')17 patcher = patch('storeapi.common.requests.get')
18 self.mock_get = patcher.start()18 self.mock_get = patcher.start()
19 self.mock_response = self.mock_get.return_value19 self.mock_response = self.mock_get.return_value
20 self.addCleanup(patcher.stop)20 self.addCleanup(patcher.stop)
@@ -42,7 +42,7 @@
42 self.assertEqual(data, expected)42 self.assertEqual(data, expected)
4343
44 def test_get_info_uses_environment_variables(self):44 def test_get_info_uses_environment_variables(self):
45 with patch('click_toolbelt.api.common.os.environ',45 with patch('storeapi.common.os.environ',
46 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):46 {'MYAPPS_API_ROOT_URL': 'http://example.com'}):
47 get_info()47 get_info()
48 self.mock_get.assert_called_once_with('http://example.com')48 self.mock_get.assert_called_once_with('http://example.com')
4949
=== modified file 'storeapi/tests/test_login.py'
--- click_toolbelt/tests/api/test_login.py 2015-12-21 18:41:57 +0000
+++ storeapi/tests/test_login.py 2016-01-08 14:21:43 +0000
@@ -8,9 +8,8 @@
8from mock import patch8from mock import patch
9from requests import Response9from requests import Response
1010
11from click_toolbelt.api._login import login11from storeapi._login import login
12from click_toolbelt.constants import (12from storeapi.constants import (
13 CLICK_TOOLBELT_PROJECT_NAME,
14 UBUNTU_SSO_API_ROOT_URL,13 UBUNTU_SSO_API_ROOT_URL,
15)14)
1615
@@ -21,12 +20,13 @@
21 super(LoginAPITestCase, self).setUp()20 super(LoginAPITestCase, self).setUp()
22 self.email = 'user@domain.com'21 self.email = 'user@domain.com'
23 self.password = 'password'22 self.password = 'password'
23 self.token_name = 'token-name'
2424
25 # setup patches25 # setup patches
26 mock_environ = {26 mock_environ = {
27 'UBUNTU_SSO_API_ROOT_URL': UBUNTU_SSO_API_ROOT_URL,27 'UBUNTU_SSO_API_ROOT_URL': UBUNTU_SSO_API_ROOT_URL,
28 }28 }
29 patcher = patch('click_toolbelt.api._login.os.environ', mock_environ)29 patcher = patch('storeapi._login.os.environ', mock_environ)
30 patcher.start()30 patcher.start()
31 self.addCleanup(patcher.stop)31 self.addCleanup(patcher.stop)
3232
@@ -51,8 +51,9 @@
51 response._content = json.dumps(data).encode('utf-8')51 response._content = json.dumps(data).encode('utf-8')
52 return response52 return response
5353
54 def assert_login_request(self, otp=None,54 def assert_login_request(self, otp=None, token_name=None):
55 token_name=CLICK_TOOLBELT_PROJECT_NAME):55 if token_name is None:
56 token_name = self.token_name
56 data = {57 data = {
57 'email': self.email,58 'email': self.email,
58 'password': self.password,59 'password': self.password,
@@ -67,12 +68,12 @@
67 )68 )
6869
69 def test_login_successful(self):70 def test_login_successful(self):
70 result = login(self.email, self.password)71 result = login(self.email, self.password, self.token_name)
71 expected = {'success': True, 'body': self.token_data}72 expected = {'success': True, 'body': self.token_data}
72 self.assertEqual(result, expected)73 self.assertEqual(result, expected)
7374
74 def test_default_token_name(self):75 def test_default_token_name(self):
75 result = login(self.email, self.password)76 result = login(self.email, self.password, self.token_name)
76 expected = {'success': True, 'body': self.token_data}77 expected = {'success': True, 'body': self.token_data}
77 self.assertEqual(result, expected)78 self.assertEqual(result, expected)
78 self.assert_login_request()79 self.assert_login_request()
@@ -84,7 +85,8 @@
84 self.assert_login_request(token_name='my-token')85 self.assert_login_request(token_name='my-token')
8586
86 def test_login_with_otp(self):87 def test_login_with_otp(self):
87 result = login(self.email, self.password, otp='123456')88 result = login(self.email, self.password, self.token_name,
89 otp='123456')
88 expected = {'success': True, 'body': self.token_data}90 expected = {'success': True, 'body': self.token_data}
89 self.assertEqual(result, expected)91 self.assertEqual(result, expected)
90 self.assert_login_request(otp='123456')92 self.assert_login_request(otp='123456')
@@ -99,7 +101,7 @@
99 status_code=401, reason='UNAUTHORISED', data=error_data)101 status_code=401, reason='UNAUTHORISED', data=error_data)
100 self.mock_request.return_value = response102 self.mock_request.return_value = response
101103
102 result = login(self.email, self.password)104 result = login(self.email, self.password, self.token_name)
103 expected = {'success': False, 'body': error_data}105 expected = {'success': False, 'body': error_data}
104 self.assertEqual(result, expected)106 self.assertEqual(result, expected)
105107
@@ -113,6 +115,6 @@
113 status_code=401, reason='UNAUTHORISED', data=error_data)115 status_code=401, reason='UNAUTHORISED', data=error_data)
114 self.mock_request.return_value = response116 self.mock_request.return_value = response
115117
116 result = login(self.email, self.password)118 result = login(self.email, self.password, self.token_name)
117 expected = {'success': False, 'body': error_data}119 expected = {'success': False, 'body': error_data}
118 self.assertEqual(result, expected)120 self.assertEqual(result, expected)
119121
=== modified file 'storeapi/tests/test_upload.py'
--- click_toolbelt/tests/api/test_upload.py 2015-12-21 18:41:57 +0000
+++ storeapi/tests/test_upload.py 2016-01-08 14:21:43 +0000
@@ -4,28 +4,26 @@
4import json4import json
5import os5import os
6import tempfile6import tempfile
7from unittest import TestCase
78
8from mock import ANY, patch9from mock import ANY, patch
9from requests import Response10from requests import Response
1011
11from click_toolbelt.api._upload import (12from storeapi._upload import (
12 get_upload_url,13 get_upload_url,
13 upload_app,14 upload_app,
14 upload_files,15 upload_files,
15 upload,16 upload,
16)17)
17from click_toolbelt.tests.test_config import (18
18 ConfigTestCase,19
19)20class UploadBaseTestCase(TestCase):
20
21
22class UploadBaseTestCase(ConfigTestCase):
2321
24 def setUp(self):22 def setUp(self):
25 super(UploadBaseTestCase, self).setUp()23 super(UploadBaseTestCase, self).setUp()
2624
27 # setup patches25 # setup patches
28 name = 'click_toolbelt.api._upload.get_oauth_session'26 name = 'storeapi._upload.get_oauth_session'
29 patcher = patch(name)27 patcher = patch(name)
30 self.mock_get_oauth_session = patcher.start()28 self.mock_get_oauth_session = patcher.start()
31 self.addCleanup(patcher.stop)29 self.addCleanup(patcher.stop)
@@ -33,14 +31,15 @@
33 self.mock_get = self.mock_get_oauth_session.return_value.get31 self.mock_get = self.mock_get_oauth_session.return_value.get
34 self.mock_post = self.mock_get_oauth_session.return_value.post32 self.mock_post = self.mock_get_oauth_session.return_value.post
3533
36
37class UploadWithScanTestCase(UploadBaseTestCase):
38
39 def setUp(self):
40 super(UploadWithScanTestCase, self).setUp()
41 self.suffix = '_0.1_all.click'34 self.suffix = '_0.1_all.click'
42 self.binary_file = self.get_temporary_file(suffix=self.suffix)35 self.binary_file = self.get_temporary_file(suffix=self.suffix)
4336
37 def get_temporary_file(self, suffix='.cfg'):
38 return tempfile.NamedTemporaryFile(suffix=suffix)
39
40
41class UploadWithScanTestCase(UploadBaseTestCase):
42
44 def test_default_metadata(self):43 def test_default_metadata(self):
45 mock_response = self.mock_post.return_value44 mock_response = self.mock_post.return_value
46 mock_response.ok = True45 mock_response.ok = True
@@ -201,7 +200,7 @@
201 self.package_name = 'namespace.binary'200 self.package_name = 'namespace.binary'
202201
203 patcher = patch.multiple(202 patcher = patch.multiple(
204 'click_toolbelt.api._upload',203 'storeapi._upload',
205 SCAN_STATUS_POLL_DELAY=0.0001)204 SCAN_STATUS_POLL_DELAY=0.0001)
206 patcher.start()205 patcher.start()
207 self.addCleanup(patcher.stop)206 self.addCleanup(patcher.stop)
@@ -450,7 +449,7 @@
450 files=[],449 files=[],
451 )450 )
452451
453 @patch('click_toolbelt.api._upload.open')452 @patch('storeapi._upload.open')
454 def test_upload_app_with_icon(self, mock_open):453 def test_upload_app_with_icon(self, mock_open):
455 with tempfile.NamedTemporaryFile() as icon:454 with tempfile.NamedTemporaryFile() as icon:
456 mock_open.return_value = icon455 mock_open.return_value = icon
@@ -473,7 +472,7 @@
473 ],472 ],
474 )473 )
475474
476 @patch('click_toolbelt.api._upload.open')475 @patch('storeapi._upload.open')
477 def test_upload_app_with_screenshots(self, mock_open):476 def test_upload_app_with_screenshots(self, mock_open):
478 screenshot1 = tempfile.NamedTemporaryFile()477 screenshot1 = tempfile.NamedTemporaryFile()
479 screenshot2 = tempfile.NamedTemporaryFile()478 screenshot2 = tempfile.NamedTemporaryFile()
480479
=== added file 'tests.py'
--- tests.py 1970-01-01 00:00:00 +0000
+++ tests.py 2016-01-08 14:21:43 +0000
@@ -0,0 +1,9 @@
1from unittest import TestLoader, TestSuite
2
3
4def load_tests(loader, tests, pattern):
5 suites = [
6 TestLoader().discover('click_toolbelt', top_level_dir='.'),
7 TestLoader().discover('storeapi', top_level_dir='.'),
8 ]
9 return TestSuite(suites)

Subscribers

People subscribed via source and target branches