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