Merge lp:~psivaa/core-image-watcher/image-watcher-udf into lp:core-image-watcher
- image-watcher-udf
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Thomi Richards |
Approved revision: | 16 |
Merged at revision: | 5 |
Proposed branch: | lp:~psivaa/core-image-watcher/image-watcher-udf |
Merge into: | lp:core-image-watcher |
Diff against target: |
443 lines (+377/-5) 9 files modified
.bzrignore (+1/-0) README.rst (+64/-0) core-image-watcher.py (+18/-0) core-service.conf (+10/-0) core_image_watcher/__init__.py (+171/-5) core_image_watcher/tests/test_image_watcher.py (+110/-0) requirements.txt (+1/-0) setup.py (+1/-0) test_requirements.txt (+1/-0) |
To merge this branch: | bzr merge lp:~psivaa/core-image-watcher/image-watcher-udf |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Celso Providelo (community) | Approve | ||
Thomi Richards (community) | Approve | ||
Joe Talbott (community) | Needs Information | ||
Review via email: mp+254122@code.launchpad.net |
Commit message
Image watcher component to publish messages to a rabbit queue when there is a new image present in the core image server.
Description of the change
Image watcher component to publish messages to a rabbit queue when there is a new image present in the core image server.
The message does not include the test branch details since it could be picked up at a later stage in the workflow. But if that has also to be taken from the same config as its used here, we could include here.
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
A few things need fixing - please don't hesitate to ping me if anything doesn't make sense here.
Overall this is looking really nice - thank you!
Celso Providelo (cprov) wrote : | # |
Psivaa,
Quick adjustment before review, the configuration file on the charms is called "core-service.
Para Siva (psivaa) wrote : | # |
Thanks Thomi, Cprov and Joe for the comments. I've addressed as much as possible. Would you be able to take another look please?
Thanks
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
Hi Siva,
I made a new review, I'm afraid there are still several things to change. Let me know if you have any questions.
Cheers,
Para Siva (psivaa) wrote : | # |
Thanks again Thomi for the review. I need to mention that I really enjoyed fixing these comments, especially when you relate them to the fundamental principles.
I have addressed all your comments. Would be great for another look. Thanks
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
Looks great - thanks!
Celso Providelo (cprov) wrote : | # |
Psivaa,
There is only one remaining point about the kombu connection context, it should have a more restricted and coherent lifetime (inside enqueue())
It might impact badly in tests, lets see how it goes.
Para Siva (psivaa) wrote : | # |
Cprove, Thomi,
Thanks a lot again for the reviews and help. Moved kombu connection bit inside enqueue and got rid of the CoreImageWatcher class.
Added a couple of unittests for _check_
Celso Providelo (cprov) wrote : | # |
Nice one! I liked the procedural version. Looking forward to see it in production.
Preview Diff
1 | === modified file '.bzrignore' | |||
2 | --- .bzrignore 2015-03-25 03:47:26 +0000 | |||
3 | +++ .bzrignore 2015-03-26 20:44:41 +0000 | |||
4 | @@ -1,1 +1,2 @@ | |||
5 | 1 | core_image_watcher.egg-info | 1 | core_image_watcher.egg-info |
6 | 2 | ve | ||
7 | 2 | 3 | ||
8 | === modified file 'README.rst' | |||
9 | --- README.rst 2015-03-24 22:58:34 +0000 | |||
10 | +++ README.rst 2015-03-26 20:44:41 +0000 | |||
11 | @@ -2,3 +2,67 @@ | |||
12 | 2 | ################## | 2 | ################## |
13 | 3 | 3 | ||
14 | 4 | A micro-service that watches for new Ubuntu Core images. | 4 | A micro-service that watches for new Ubuntu Core images. |
15 | 5 | |||
16 | 6 | Get the Source | ||
17 | 7 | ============== | ||
18 | 8 | |||
19 | 9 | Branch the code:: | ||
20 | 10 | |||
21 | 11 | $ bzr branch lp:core-image-watcher | ||
22 | 12 | |||
23 | 13 | Install the Service | ||
24 | 14 | ======================== | ||
25 | 15 | |||
26 | 16 | Install system dependencies:: | ||
27 | 17 | |||
28 | 18 | $ sudo apt-get install python3-dev | ||
29 | 19 | |||
30 | 20 | Build and activate a virtualenv with python3:: | ||
31 | 21 | |||
32 | 22 | $ virtualenv -p python3 --system-site-packages ve | ||
33 | 23 | $ . ve/bin/activate | ||
34 | 24 | |||
35 | 25 | Install dependencies from pypi:: | ||
36 | 26 | |||
37 | 27 | $ pip install -r requirements.txt | ||
38 | 28 | |||
39 | 29 | ...and install some dependencies from phablet-tools PPA:: | ||
40 | 30 | |||
41 | 31 | $ sudo add-apt-repository ppa:phablet-team/tools | ||
42 | 32 | $ sudo apt-get update | ||
43 | 33 | $ sudo apt-get install ubuntu-device-flash | ||
44 | 34 | |||
45 | 35 | Install the service itself:: | ||
46 | 36 | |||
47 | 37 | $ python setup.py install | ||
48 | 38 | |||
49 | 39 | ...you may want to install it in 'development mode', which symlinks files, | ||
50 | 40 | so you can edit/re-run without having to re-install the service. In that | ||
51 | 41 | case, run:: | ||
52 | 42 | |||
53 | 43 | $ python setup.py develop | ||
54 | 44 | |||
55 | 45 | Run the tests! | ||
56 | 46 | ============== | ||
57 | 47 | |||
58 | 48 | Install dependencies:: | ||
59 | 49 | |||
60 | 50 | $ pip install -r test_requirements.txt | ||
61 | 51 | |||
62 | 52 | Run those tests - with vigour!:: | ||
63 | 53 | |||
64 | 54 | $ python setup.py test | ||
65 | 55 | |||
66 | 56 | The config file | ||
67 | 57 | =============== | ||
68 | 58 | |||
69 | 59 | The sample configuration file in 'core-service.conf':: | ||
70 | 60 | |||
71 | 61 | [amqp] | ||
72 | 62 | uris = amqp://guest:guest@localhost:5672// | ||
73 | 63 | |||
74 | 64 | [image] | ||
75 | 65 | channel = devel-proposed | ||
76 | 66 | device = generic_amd64 | ||
77 | 67 | location = /tmp/latest-core-image-version | ||
78 | 68 | poll_period = 60 | ||
79 | 5 | 69 | ||
80 | === modified file 'core-image-watcher.py' | |||
81 | --- core-image-watcher.py 2015-03-25 02:36:54 +0000 | |||
82 | +++ core-image-watcher.py 2015-03-26 20:44:41 +0000 | |||
83 | @@ -1,3 +1,21 @@ | |||
84 | 1 | #!/usr/bin/env python3 | ||
85 | 2 | |||
86 | 3 | # core-image-watcher | ||
87 | 4 | # Copyright (C) 2015 Canonical | ||
88 | 5 | # | ||
89 | 6 | # This program is free software: you can redistribute it and/or modify | ||
90 | 7 | # it under the terms of the GNU General Public License as published by | ||
91 | 8 | # the Free Software Foundation, either version 3 of the License, or | ||
92 | 9 | # (at your option) any later version. | ||
93 | 10 | # | ||
94 | 11 | # This program is distributed in the hope that it will be useful, | ||
95 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
96 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
97 | 14 | # GNU General Public License for more details. | ||
98 | 15 | # | ||
99 | 16 | # You should have received a copy of the GNU General Public License | ||
100 | 17 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
101 | 18 | # | ||
102 | 1 | 19 | ||
103 | 2 | from core_image_watcher import main | 20 | from core_image_watcher import main |
104 | 3 | 21 | ||
105 | 4 | 22 | ||
106 | === added file 'core-service.conf' | |||
107 | --- core-service.conf 1970-01-01 00:00:00 +0000 | |||
108 | +++ core-service.conf 2015-03-26 20:44:41 +0000 | |||
109 | @@ -0,0 +1,10 @@ | |||
110 | 1 | # `core-image-watcher` configuration file. | ||
111 | 2 | |||
112 | 3 | [amqp] | ||
113 | 4 | uris = amqp://guest:guest@localhost:5672// | ||
114 | 5 | |||
115 | 6 | [image] | ||
116 | 7 | channel = devel-proposed | ||
117 | 8 | device = generic_amd64 | ||
118 | 9 | location = /tmp/latest-core-image-version | ||
119 | 10 | poll_period = 60 | ||
120 | 0 | 11 | ||
121 | === modified file 'core_image_watcher/__init__.py' | |||
122 | --- core_image_watcher/__init__.py 2015-03-25 02:16:17 +0000 | |||
123 | +++ core_image_watcher/__init__.py 2015-03-26 20:44:41 +0000 | |||
124 | @@ -1,8 +1,174 @@ | |||
128 | 1 | 1 | # core-image-watcher | |
129 | 2 | 2 | # Copyright (C) 2015 Canonical | |
130 | 3 | import select | 3 | # |
131 | 4 | # This program is free software: you can redistribute it and/or modify | ||
132 | 5 | # it under the terms of the GNU General Public License as published by | ||
133 | 6 | # the Free Software Foundation, either version 3 of the License, or | ||
134 | 7 | # (at your option) any later version. | ||
135 | 8 | # | ||
136 | 9 | # This program is distributed in the hope that it will be useful, | ||
137 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
138 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
139 | 12 | # GNU General Public License for more details. | ||
140 | 13 | # | ||
141 | 14 | # You should have received a copy of the GNU General Public License | ||
142 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
143 | 16 | # | ||
144 | 17 | |||
145 | 18 | """Core Image Watcher functional code.""" | ||
146 | 19 | |||
147 | 20 | import argparse | ||
148 | 21 | import configparser | ||
149 | 22 | import logging | ||
150 | 23 | import os | ||
151 | 24 | import subprocess | ||
152 | 25 | import time | ||
153 | 26 | |||
154 | 27 | import kombu | ||
155 | 28 | |||
156 | 29 | logger = logging.getLogger(__name__) | ||
157 | 30 | |||
158 | 31 | API_VERSION = "v1" | ||
159 | 32 | |||
160 | 33 | |||
161 | 34 | def _enqueue(body, amqp_uris): | ||
162 | 35 | """Enqueues a message of `body` to the rabbit queue""" | ||
163 | 36 | with kombu.Connection(amqp_uris) as connection: | ||
164 | 37 | logger.info('Triggering request: %s', body, extra=body) | ||
165 | 38 | try: | ||
166 | 39 | queue = connection.SimpleQueue( | ||
167 | 40 | 'core.image.{}'.format(API_VERSION)) | ||
168 | 41 | queue.put(body) | ||
169 | 42 | queue.close() | ||
170 | 43 | except Exception as exc: | ||
171 | 44 | logger.error(exc, exc_info=True) | ||
172 | 45 | logger.info('Done!', extra=body) | ||
173 | 46 | |||
174 | 47 | |||
175 | 48 | def _get_version_string_output(channel, device): | ||
176 | 49 | """Obtains a bytestring of the images info from the core image server""" | ||
177 | 50 | cmd = ['ubuntu-device-flash', | ||
178 | 51 | 'query ', | ||
179 | 52 | '--list-images', | ||
180 | 53 | '--channel', | ||
181 | 54 | 'ubuntu-core/{}'.format(channel), | ||
182 | 55 | '--device={}'.format(device)] | ||
183 | 56 | images = None | ||
184 | 57 | try: | ||
185 | 58 | images = subprocess.check_output(cmd) | ||
186 | 59 | except subprocess.CalledProcessError as e: | ||
187 | 60 | logger.error(e, exc_info=True) | ||
188 | 61 | finally: | ||
189 | 62 | return images | ||
190 | 63 | |||
191 | 64 | |||
192 | 65 | def _get_latest_image_version(channel, | ||
193 | 66 | device, | ||
194 | 67 | get_output=_get_version_string_output): | ||
195 | 68 | """Returns largest image version""" | ||
196 | 69 | images = get_output(channel, device).split() | ||
197 | 70 | if (len(images) >= 2): | ||
198 | 71 | return images[-2].decode('utf-8').replace(':', '') | ||
199 | 72 | else: | ||
200 | 73 | logger.info('Could not get the image list by u-d-f') | ||
201 | 74 | return None | ||
202 | 75 | |||
203 | 76 | |||
204 | 77 | def _cache_version_to_disk(location, latest_version): | ||
205 | 78 | """Caches the version of the image if it's larger than a cached one""" | ||
206 | 79 | try: | ||
207 | 80 | if os.path.exists(location): | ||
208 | 81 | with open(location, 'r') as cache: | ||
209 | 82 | if (int(cache.read()) < int(latest_version)): | ||
210 | 83 | with open(location, 'w') as f: | ||
211 | 84 | f.write(latest_version) | ||
212 | 85 | else: | ||
213 | 86 | latest_version = None | ||
214 | 87 | else: | ||
215 | 88 | with open(location, 'w') as f: | ||
216 | 89 | f.write(latest_version) | ||
217 | 90 | except IOError as e: | ||
218 | 91 | latest_version = None | ||
219 | 92 | logger.error('Writing the latest image info failed: %s', (str(e))) | ||
220 | 93 | finally: | ||
221 | 94 | return latest_version | ||
222 | 95 | |||
223 | 96 | |||
224 | 97 | def _check_for_new_image(location, | ||
225 | 98 | channel, | ||
226 | 99 | device, | ||
227 | 100 | latest_image_version=_get_latest_image_version, | ||
228 | 101 | cached_version=_cache_version_to_disk): | ||
229 | 102 | """Check if a new image is present in the core image server""" | ||
230 | 103 | latest_version = latest_image_version(channel, device) | ||
231 | 104 | try: | ||
232 | 105 | ret = cached_version(location, latest_version) | ||
233 | 106 | if not ret: | ||
234 | 107 | # There is no new image | ||
235 | 108 | # Do not progress | ||
236 | 109 | return None | ||
237 | 110 | except Exception as e: | ||
238 | 111 | logger.error(e, exc_info=True) | ||
239 | 112 | return None | ||
240 | 113 | body = {} | ||
241 | 114 | body['image_name'] = latest_version | ||
242 | 115 | body['channel'] = channel | ||
243 | 116 | body['device'] = device | ||
244 | 117 | return body | ||
245 | 118 | |||
246 | 119 | |||
247 | 120 | def configure_logging(config): | ||
248 | 121 | root_logger = logging.getLogger() | ||
249 | 122 | root_logger.setLevel(logging.INFO) | ||
250 | 123 | |||
251 | 124 | requests_logger = logging.getLogger('requests') | ||
252 | 125 | requests_logger.setLevel(logging.WARNING) | ||
253 | 126 | |||
254 | 127 | # If there is no ./logs directory, fallback to stderr. | ||
255 | 128 | log_path = os.path.abspath( | ||
256 | 129 | os.path.join(__file__, '../../logs/core-image-watcher.log')) | ||
257 | 130 | log_dir = os.path.dirname(log_path) | ||
258 | 131 | if os.path.exists(log_dir): | ||
259 | 132 | handler = logging.FileHandler(log_path) | ||
260 | 133 | else: | ||
261 | 134 | print("'logs' directory '{}' does not exist, using stderr " | ||
262 | 135 | "for app log.".format(log_dir)) | ||
263 | 136 | handler = logging.StreamHandler() | ||
264 | 137 | |||
265 | 138 | handler.setFormatter( | ||
266 | 139 | logging.Formatter( | ||
267 | 140 | '%(asctime)s %(name)s %(levelname)s: %(message)s' | ||
268 | 141 | ) | ||
269 | 142 | ) | ||
270 | 143 | root_logger.addHandler(handler) | ||
271 | 4 | 144 | ||
272 | 5 | 145 | ||
273 | 6 | def main(): | 146 | def main(): |
276 | 7 | # for now, do nothing at all. | 147 | parser = argparse.ArgumentParser( |
277 | 8 | select.select([], [], []) | 148 | description='Core image watcher ...') |
278 | 149 | parser.add_argument('-c', '--conf', default='core-service.conf', | ||
279 | 150 | help='Configuration file path') | ||
280 | 151 | args = parser.parse_args() | ||
281 | 152 | |||
282 | 153 | # Load configuration options. | ||
283 | 154 | config = configparser.ConfigParser() | ||
284 | 155 | config.read(args.conf) | ||
285 | 156 | |||
286 | 157 | configure_logging(config) | ||
287 | 158 | |||
288 | 159 | amqp_uris = config.get('amqp', 'uris').split() | ||
289 | 160 | location = config.get('image', 'location') | ||
290 | 161 | channel = config.get('image', 'channel') | ||
291 | 162 | device = config.get('image', 'device') | ||
292 | 163 | poll_period = float(config.get('image', 'poll_period')) | ||
293 | 164 | |||
294 | 165 | try: | ||
295 | 166 | while True: | ||
296 | 167 | message_body = _check_for_new_image(location, | ||
297 | 168 | channel, | ||
298 | 169 | device) | ||
299 | 170 | if message_body: | ||
300 | 171 | _enqueue(message_body, amqp_uris) | ||
301 | 172 | time.sleep(poll_period) | ||
302 | 173 | except KeyboardInterrupt: | ||
303 | 174 | print('Bye!') | ||
304 | 9 | 175 | ||
305 | === added directory 'core_image_watcher/tests' | |||
306 | === added file 'core_image_watcher/tests/__init__.py' | |||
307 | === added file 'core_image_watcher/tests/test_image_watcher.py' | |||
308 | --- core_image_watcher/tests/test_image_watcher.py 1970-01-01 00:00:00 +0000 | |||
309 | +++ core_image_watcher/tests/test_image_watcher.py 2015-03-26 20:44:41 +0000 | |||
310 | @@ -0,0 +1,110 @@ | |||
311 | 1 | # Ubuntu CI Engine | ||
312 | 2 | # Copyright 2015 Canonical Ltd. | ||
313 | 3 | |||
314 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
315 | 5 | # under the terms of the GNU Affero General Public License version 3, as | ||
316 | 6 | # published by the Free Software Foundation. | ||
317 | 7 | |||
318 | 8 | # This program is distributed in the hope that it will be useful, but | ||
319 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
320 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
321 | 11 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
322 | 12 | |||
323 | 13 | # You should have received a copy of the GNU Affero General Public License | ||
324 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
325 | 15 | |||
326 | 16 | import os | ||
327 | 17 | import tempfile | ||
328 | 18 | import testtools | ||
329 | 19 | |||
330 | 20 | from core_image_watcher import ( | ||
331 | 21 | _cache_version_to_disk, | ||
332 | 22 | _get_latest_image_version, | ||
333 | 23 | _check_for_new_image, | ||
334 | 24 | ) | ||
335 | 25 | |||
336 | 26 | |||
337 | 27 | class TestWatchImage(testtools.TestCase): | ||
338 | 28 | |||
339 | 29 | def write_to_tempfile(self, version): | ||
340 | 30 | temp_file = tempfile.NamedTemporaryFile('w+', delete=False) | ||
341 | 31 | with temp_file as f: | ||
342 | 32 | f.write(version) | ||
343 | 33 | self.addCleanup(os.remove, temp_file.name) | ||
344 | 34 | return temp_file.name | ||
345 | 35 | |||
346 | 36 | def test_cache_version_to_disk(self): | ||
347 | 37 | temp_file_name = self.write_to_tempfile('111') | ||
348 | 38 | return_value = _cache_version_to_disk(temp_file_name, '112') | ||
349 | 39 | self.assertEqual(return_value, '112') | ||
350 | 40 | |||
351 | 41 | def test_cache_version_with_different_digits_to_disk(self): | ||
352 | 42 | temp_file_name = self.write_to_tempfile('23') | ||
353 | 43 | return_value = _cache_version_to_disk(temp_file_name, '112') | ||
354 | 44 | self.assertEqual(return_value, '112') | ||
355 | 45 | |||
356 | 46 | def test_do_not_cache_the_same_version_to_disk(self): | ||
357 | 47 | temp_file_name = self.write_to_tempfile('112') | ||
358 | 48 | return_value = _cache_version_to_disk(temp_file_name, '112') | ||
359 | 49 | self.assertEqual(return_value, None) | ||
360 | 50 | |||
361 | 51 | def test_do_not_proceed(self): | ||
362 | 52 | temp_file_name = self.write_to_tempfile('113') | ||
363 | 53 | return_value = _cache_version_to_disk(temp_file_name, '112') | ||
364 | 54 | self.assertEqual(return_value, None) | ||
365 | 55 | |||
366 | 56 | def test_freshly_cache_version_to_disk(self): | ||
367 | 57 | temp_file = tempfile.NamedTemporaryFile('w+', delete=False) | ||
368 | 58 | return_value = _cache_version_to_disk(temp_file.name, '112') | ||
369 | 59 | self.addCleanup(os.remove, temp_file.name) | ||
370 | 60 | self.assertEqual(return_value, '112') | ||
371 | 61 | |||
372 | 62 | def test_get_latest_version(self): | ||
373 | 63 | def get_output(channel, device): | ||
374 | 64 | return b"295: description='fake295'\n\ | ||
375 | 65 | 296: description='fake296'\n\ | ||
376 | 66 | 297: description='fake297'\n\ | ||
377 | 67 | 298: description='fake338'\n" | ||
378 | 68 | observed = _get_latest_image_version('fakechannel', | ||
379 | 69 | 'fakedevice', | ||
380 | 70 | get_output) | ||
381 | 71 | self.assertEqual(observed, '298') | ||
382 | 72 | |||
383 | 73 | def test_dont_get_latest_version(self): | ||
384 | 74 | def get_output(channel, device): | ||
385 | 75 | return b"100: description='fake100'\n" | ||
386 | 76 | observed = _get_latest_image_version('fakechannel', | ||
387 | 77 | 'fakedevice', | ||
388 | 78 | get_output) | ||
389 | 79 | self.assertEqual(observed, '100') | ||
390 | 80 | |||
391 | 81 | def test_check_for_new_image(self): | ||
392 | 82 | def _get_latest_image_version(channel, device): | ||
393 | 83 | return '100' | ||
394 | 84 | |||
395 | 85 | def _cache_version_to_disk(location, latest_version): | ||
396 | 86 | return '99' | ||
397 | 87 | |||
398 | 88 | body = _check_for_new_image('fakelocation', | ||
399 | 89 | 'fakechannel', | ||
400 | 90 | 'fakedevice', | ||
401 | 91 | _get_latest_image_version, | ||
402 | 92 | _cache_version_to_disk) | ||
403 | 93 | self.assertEqual(body, | ||
404 | 94 | {'device': 'fakedevice', | ||
405 | 95 | 'image_name': '100', | ||
406 | 96 | 'channel': 'fakechannel'}) | ||
407 | 97 | |||
408 | 98 | def test_check_for_no_new_image(self): | ||
409 | 99 | def _get_latest_image_version(channel, device): | ||
410 | 100 | return '100' | ||
411 | 101 | |||
412 | 102 | def _cache_version_to_disk(location, latest_version): | ||
413 | 103 | return None | ||
414 | 104 | |||
415 | 105 | body = _check_for_new_image('fakelocation', | ||
416 | 106 | 'fakechannel', | ||
417 | 107 | 'fakedevice', | ||
418 | 108 | _get_latest_image_version, | ||
419 | 109 | _cache_version_to_disk) | ||
420 | 110 | self.assertEqual(body, None) | ||
421 | 0 | 111 | ||
422 | === modified file 'requirements.txt' | |||
423 | --- requirements.txt 2015-03-25 01:34:08 +0000 | |||
424 | +++ requirements.txt 2015-03-26 20:44:41 +0000 | |||
425 | @@ -0,0 +1,1 @@ | |||
426 | 1 | kombu==3.0.24 | ||
427 | 0 | 2 | ||
428 | === modified file 'setup.py' | |||
429 | --- setup.py 2015-03-25 03:47:26 +0000 | |||
430 | +++ setup.py 2015-03-26 20:44:41 +0000 | |||
431 | @@ -36,5 +36,6 @@ | |||
432 | 36 | url='https://launchpad.net/core-image-watcher', | 36 | url='https://launchpad.net/core-image-watcher', |
433 | 37 | license='GPLv3', | 37 | license='GPLv3', |
434 | 38 | packages=find_packages(), | 38 | packages=find_packages(), |
435 | 39 | test_suite='core_image_watcher.tests', | ||
436 | 39 | scripts=['core-image-watcher.py'] | 40 | scripts=['core-image-watcher.py'] |
437 | 40 | ) | 41 | ) |
438 | 41 | 42 | ||
439 | === modified file 'test_requirements.txt' | |||
440 | --- test_requirements.txt 2015-03-25 01:34:08 +0000 | |||
441 | +++ test_requirements.txt 2015-03-26 20:44:41 +0000 | |||
442 | @@ -0,0 +1,1 @@ | |||
443 | 1 | testtools==1.7.1 |
A few in-line questions.