Merge lp:~matiasb/click-toolbelt/get-channel-details into lp:click-toolbelt

Proposed by Matias Bordese
Status: Merged
Approved by: Matias Bordese
Approved revision: 41
Merged at revision: 40
Proposed branch: lp:~matiasb/click-toolbelt/get-channel-details
Merge into: lp:click-toolbelt
Diff against target: 224 lines (+193/-1)
4 files modified
click_toolbelt/channels.py (+64/-0)
click_toolbelt/tests/test_channels.py (+128/-0)
click_toolbelt/tests/test_upload.py (+0/-1)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~matiasb/click-toolbelt/get-channel-details
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+279514@code.launchpad.net

Commit message

Added initial command for channels handling.

To post a comment you must log in.
Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'click_toolbelt/channels.py'
2--- click_toolbelt/channels.py 1970-01-01 00:00:00 +0000
3+++ click_toolbelt/channels.py 2015-12-04 18:34:17 +0000
4@@ -0,0 +1,64 @@
5+# Copyright 2015 Canonical Ltd. This software is licensed under the
6+# GNU General Public License version 3 (see the file LICENSE).
7+from __future__ import absolute_import, unicode_literals
8+import logging
9+import os
10+
11+from click_toolbelt.common import (
12+ AuthenticatedCommand,
13+ CommandError,
14+)
15+from click_toolbelt.compat import urljoin
16+from click_toolbelt.constants import MYAPPS_API_ROOT_URL
17+
18+
19+class Channels(AuthenticatedCommand):
20+ """Get/Update channels configuration for a package."""
21+
22+ log = logging.getLogger(__name__)
23+
24+ def get_parser(self, prog_name):
25+ parser = super(Channels, self).get_parser(prog_name)
26+ parser.add_argument('package_name')
27+ return parser
28+
29+ def get_channels(self, session, package_name):
30+ """Get current channels config for package through API."""
31+ result = {'success': False, 'errors': [], 'data': None}
32+ myapps_url = os.environ.get('MYAPPS_API_ROOT_URL', MYAPPS_API_ROOT_URL)
33+ channels_endpoint = urljoin(
34+ myapps_url, 'package-channels/%s/' % package_name)
35+
36+ response = session.get(channels_endpoint)
37+ if response.ok:
38+ result['success'] = True
39+ result['data'] = response.json()
40+ else:
41+ result['errors'] = [response.text]
42+ return result
43+
44+ def take_action(self, parsed_args):
45+ package_name = parsed_args.package_name
46+
47+ session = self.get_oauth_session()
48+ if session is None:
49+ self.log.info('No valid credentials found.')
50+ # raise an exception to exit with proper code
51+ raise CommandError()
52+
53+ result = self.get_channels(session, package_name)
54+
55+ if not result.get('success', False):
56+ self.log.info(
57+ 'Could not get information. An error ocurred:\n\n%s\n\n',
58+ '\n'.join(result['errors']))
59+ # raise an exception to exit with proper code
60+ raise CommandError()
61+
62+ data = result['data']
63+ for config in data:
64+ value = config.get('current')
65+ if value is not None:
66+ value = 'Sequence %d (version %s)' % (
67+ value.get('sequence'), value.get('version'))
68+ self.log.info('%s: %s', config.get('channel'), value)
69
70=== added file 'click_toolbelt/tests/test_channels.py'
71--- click_toolbelt/tests/test_channels.py 1970-01-01 00:00:00 +0000
72+++ click_toolbelt/tests/test_channels.py 2015-12-04 18:34:17 +0000
73@@ -0,0 +1,128 @@
74+# -*- coding: utf-8 -*-
75+# Copyright 2015 Canonical Ltd. This software is licensed under the
76+# GNU General Public License version 3 (see the file LICENSE).
77+from __future__ import absolute_import, unicode_literals
78+from collections import namedtuple
79+from unittest import TestCase
80+
81+from mock import call, patch
82+
83+from click_toolbelt.common import CommandError
84+from click_toolbelt.channels import (
85+ Channels,
86+)
87+
88+
89+class ChannelsCommandTestCase(TestCase):
90+
91+ def setUp(self):
92+ super(ChannelsCommandTestCase, self).setUp()
93+ app = None
94+ args = None
95+ self.command = Channels(app, args)
96+
97+ patcher = patch('click_toolbelt.channels.Channels.log')
98+ self.mock_log = patcher.start()
99+ self.addCleanup(patcher.stop)
100+
101+ oauth_session = 'click_toolbelt.channels.Channels.get_oauth_session'
102+ patcher = patch(oauth_session)
103+ self.mock_get_oauth_session = patcher.start()
104+ self.mock_session = self.mock_get_oauth_session.return_value
105+ self.mock_get = self.mock_session.get
106+ self.mock_response = self.mock_get.return_value
107+ self.addCleanup(patcher.stop)
108+
109+ self.parsed_args = namedtuple('parsed_args', 'package_name')
110+ self.args = self.parsed_args('package.name')
111+
112+ self.channels_data = [
113+ {'channel': 'stable', 'current': {'sequence': 2, 'version': '1'}},
114+ {'channel': 'beta', 'current': {'sequence': 4, 'version': '1.5'}},
115+ {'channel': 'edge', 'current': None},
116+ ]
117+
118+ def set_channels_success_response(self):
119+ self.mock_response.ok = True
120+ self.mock_response.json.return_value = self.channels_data
121+
122+ def set_channels_error_response(self, error_msg):
123+ self.mock_response.ok = False
124+ self.mock_response.text = error_msg
125+
126+ def test_parser(self):
127+ parser = self.command.get_parser('prog_name')
128+ # only one argument -- the first item is the default help option
129+ self.assertEqual(len(parser._actions), 2)
130+ self.assertEqual(parser._actions[0].dest, 'help')
131+ # package_name is optional
132+ self.assertEqual(parser._actions[1].dest, 'package_name')
133+ self.assertTrue(parser._actions[1].required)
134+
135+ def test_get_channels(self):
136+ self.set_channels_success_response()
137+
138+ data = self.command.get_channels(self.mock_session, 'package.name')
139+
140+ expected = {
141+ 'success': True,
142+ 'errors': [],
143+ 'data': self.channels_data,
144+ }
145+ self.assertEqual(data, expected)
146+
147+ def test_get_channels_with_error_response(self):
148+ error_msg = 'some error'
149+ self.set_channels_error_response(error_msg)
150+
151+ data = self.command.get_channels(self.mock_session, 'package.name')
152+
153+ expected = {
154+ 'success': False,
155+ 'errors': [error_msg],
156+ 'data': None,
157+ }
158+ self.assertEqual(data, expected)
159+
160+ def test_get_channels_uses_environment_variables(self):
161+ with patch('click_toolbelt.channels.os.environ',
162+ {'MYAPPS_API_ROOT_URL': 'http://example.com'}):
163+ self.command.get_channels(self.mock_session, 'package.name')
164+ self.mock_get.assert_called_once_with(
165+ 'http://example.com/package-channels/package.name/')
166+
167+ def test_take_action_invalid_credentials(self):
168+ self.mock_get_oauth_session.return_value = None
169+
170+ with self.assertRaises(CommandError):
171+ self.command.take_action(self.args)
172+
173+ self.mock_log.info.assert_called_once_with(
174+ 'No valid credentials found.')
175+
176+ def test_take_action(self):
177+ self.set_channels_success_response()
178+
179+ self.command.take_action(self.args)
180+
181+ expected_calls = []
182+ for config in self.channels_data:
183+ expected = None
184+ upload = config['current']
185+ if upload is not None:
186+ expected = 'Sequence %d (version %s)' % (
187+ upload['sequence'], upload['version'])
188+ channel_call = call('%s: %s', config['channel'], expected)
189+ expected_calls.append(channel_call)
190+ self.assertEqual(self.mock_log.info.call_args_list, expected_calls)
191+
192+ def test_take_action_with_error(self):
193+ error_msg = 'some error'
194+ self.set_channels_error_response(error_msg)
195+
196+ with self.assertRaises(CommandError):
197+ self.command.take_action(self.args)
198+
199+ self.mock_log.info.assert_called_once_with(
200+ 'Could not get information. An error ocurred:\n\n%s\n\n',
201+ 'some error')
202
203=== modified file 'click_toolbelt/tests/test_upload.py'
204--- click_toolbelt/tests/test_upload.py 2015-12-03 14:45:57 +0000
205+++ click_toolbelt/tests/test_upload.py 2015-12-04 18:34:17 +0000
206@@ -5,7 +5,6 @@
207 import os
208 import tempfile
209 from collections import namedtuple
210-from unittest import TestCase, skip
211
212 from mock import ANY, DEFAULT, patch
213 from requests import Response
214
215=== modified file 'setup.py'
216--- setup.py 2015-09-23 12:40:58 +0000
217+++ setup.py 2015-12-04 18:34:17 +0000
218@@ -61,6 +61,7 @@
219 'login = click_toolbelt.login:Login',
220 'upload = click_toolbelt.upload:Upload',
221 'info = click_toolbelt.info:Info',
222+ 'channels = click_toolbelt.channels:Channels',
223 ],
224 },
225

Subscribers

People subscribed via source and target branches