Merge lp:~james-w/pkgme-service/review-click into lp:pkgme-service

Proposed by James Westby
Status: Merged
Approved by: James Westby
Approved revision: no longer in the source branch.
Merged at revision: 143
Proposed branch: lp:~james-w/pkgme-service/review-click
Merge into: lp:pkgme-service
Diff against target: 455 lines (+133/-128)
4 files modified
src/djpkgme/client.py (+6/-11)
src/djpkgme/tasks.py (+107/-97)
src/djpkgme/tests/test_handlers.py (+2/-2)
src/djpkgme/tests/test_tasks.py (+18/-18)
To merge this branch: bzr merge lp:~james-w/pkgme-service/review-click
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+190241@code.launchpad.net

Commit message

Split out the logic of packaging an app from the mechanics of handling callbacks.

We want to re-use the callback logic for reviewing click packages, so split up
the existing code to make that separation clearer.

Description of the change

Hi,

This is the first of my stack of branches to add reviewing of click
packages to pkgme-service.

It splits the logic of running some code and sending callback/errbacks to
sca from the logic of packaging an application. We'll then be able to glue
in the click review code as an alternative codepath while re-using the
callback code.

Thanks,

James

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

This is largely a moving of code, with a couple of changes such as moving the
json.dumps in to the submit_to_myapps function.

Thanks,

James

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

LGTM

review: Approve
143. By James Westby

[r=ricardokirkner] Split out the logic of packaging an app from the mechanics of handling callbacks.

We want to re-use the callback logic for reviewing click packages, so split up
the existing code to make that separation clearer.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/djpkgme/client.py'
2--- src/djpkgme/client.py 2012-09-14 13:59:48 +0000
3+++ src/djpkgme/client.py 2013-10-09 20:40:35 +0000
4@@ -41,13 +41,13 @@
5 """Called when any error happens during package build."""
6 body = get_response_dict(None, metadata)
7 body['error'] = error_data
8- submit_to_myapps(metadata['errback_url'], json.dumps(body),
9- logger=logger)
10-
11-
12-def submit_to_myapps(url, json_body, logger=None):
13- """Put ``json_body`` to MyApps at ``url``."""
14+ submit_to_myapps(metadata['errback_url'], body, logger=logger)
15+
16+
17+def submit_to_myapps(url, body, logger=None):
18+ """Put ``body`` json-encoded to MyApps at ``url``."""
19 GOOD_RESPONSES = (200,)
20+ json_body = json.dumps(body)
21
22 headers = {}
23 if url.startswith(settings.MYAPPS_BASE_URL):
24@@ -66,11 +66,6 @@
25 raise CallbackError(url, 'PUT', response.status, content)
26
27
28-def submit_pkgme_info(packaged_app_url, metadata, logger=None):
29- body = json.dumps(get_response_dict(packaged_app_url, metadata))
30- return submit_to_myapps(metadata['callback_url'], body, logger=logger)
31-
32-
33 def get_response_dict(packaged_app_url, metadata):
34 """Return a json-serializable guess set for the myapps callback."""
35
36
37=== modified file 'src/djpkgme/tasks.py'
38--- src/djpkgme/tasks.py 2012-11-13 14:51:44 +0000
39+++ src/djpkgme/tasks.py 2013-10-09 20:40:35 +0000
40@@ -26,8 +26,9 @@
41
42 from .client import (
43 CallbackError,
44+ get_response_dict,
45 submit_error,
46- submit_pkgme_info,
47+ submit_to_myapps,
48 )
49 from .osutils import (
50 download_file,
51@@ -164,15 +165,116 @@
52 PkgmeError,
53 )
54
55- # The name of the directory that we store the downloaded file in.
56- DOWNLOAD_DIRECTORY_NAME = 'download'
57-
58 REQUIRED_FIELDS = [
59 'callback_url',
60 'package_url',
61 ]
62
63- def build_package(self, metadata, overrides):
64+ def _submit_error(self, metadata, error_data):
65+ if metadata.get('errback_url', None):
66+ self.get_logger().error(
67+ "Submitting error info to %s" % (metadata['errback_url'],))
68+ try:
69+ submit_error(metadata, error_data, logger=self.get_logger())
70+ except CallbackError, e:
71+ # XXX: Should generate an OOPS report and log the OOPS number.
72+ # Obviously we can't send to the client.
73+ self.get_logger().error(
74+ "Error while trying to submit error to client: %s\n%s"
75+ % (str(e), error_data))
76+ else:
77+ raise
78+
79+ def _validate_metadata(self, metadata):
80+ """Make sure 'metadata' has all the fields we need."""
81+ missing_fields = []
82+ for field in self.REQUIRED_FIELDS:
83+ if field not in metadata:
84+ missing_fields.append(field)
85+ if missing_fields:
86+ raise MissingFieldsError(metadata, missing_fields)
87+
88+ def _log_failure_exception(self, metadata, ftype):
89+ self.get_logger().info("%s (%s) failed with %s exception: %s" % (
90+ ftype,
91+ metadata.get('myapps_id', '<unknown myapps id>'),
92+ metadata.get('package_url', '<unknown package url>'),
93+ traceback.format_exc(),
94+ ))
95+
96+ def run(self, metadata, overrides):
97+ """Build a source package.
98+
99+ Requires 'metadata' to have:
100+ * callback_url
101+ * package_url
102+
103+ Uses these if they exist:
104+ * errback_url
105+ * myapps_id
106+
107+ Assumes that the backend can determine a non-empty value for
108+ 'package_name'.
109+ """
110+ self._validate_metadata(metadata)
111+ self.get_logger().info(
112+ "Building package for %s: %s" % (
113+ metadata.get('myapps_id', '<unknown myapps id>'),
114+ metadata.get('package_url', '<unknown package url>'),
115+ ))
116+ job = PackageAppJob()
117+ return self.run_with_error_handling(job, metadata, overrides)
118+
119+ def run_with_error_handling(self, job, metadata, overrides):
120+ try:
121+ generated_info = job.run(metadata, overrides, self.get_logger())
122+ except self.EXPECTED_EXCEPTIONS, e:
123+ # pkgme couldn't make the package. If the user were running
124+ # 'pkgme' on the command line they would get a simple,
125+ # user-friendly error. Just send that, and not the whole
126+ # traceback.
127+ self._log_failure_exception(metadata, "user")
128+ self._submit_error(metadata, str(e))
129+ except FATAL_EXCEPTIONS:
130+ # Someone hit Ctrl-C or something like that. Just keep raising.
131+ raise
132+ except:
133+ # The exception is something unexpected, and thus represents a bug
134+ # in the system.
135+ #
136+ # XXX: Should generate an OOPS and report that to the client. For
137+ # now, send the error across without the traceback. This is an
138+ # interim measure to reduce our reliance on timeouts for doing
139+ # system testing.
140+ self._log_failure_exception(metadata, "internal")
141+ exc_type, exc_obj, tb = sys.exc_info()
142+ self._submit_error(
143+ metadata, exc_info_as_dict((exc_type, exc_obj, None)))
144+ else:
145+ self.get_logger().info(
146+ "Submitting success info to %r: %s"
147+ % (metadata['callback_url'], generated_info))
148+ try:
149+ callback_body = job.transform_results(
150+ generated_info, metadata)
151+ submit_to_myapps(metadata['callback_url'], callback_body,
152+ logger=self.get_logger())
153+ except CallbackError:
154+ self._submit_error(metadata, exc_info_as_dict(sys.exc_info()))
155+
156+tasks.register(BuildPackageTask)
157+
158+
159+class PackageAppJob(object):
160+
161+ # The name of the directory that we store the downloaded file in.
162+ DOWNLOAD_DIRECTORY_NAME = 'download'
163+
164+ def transform_results(self, package_path, metadata):
165+ package_url = get_url_for_file(package_path)
166+ return get_response_dict(package_url, metadata)
167+
168+ def run(self, metadata, overrides, logger):
169 """Build packaging for 'metadata'.
170
171 :return: A path on disk to a tarball containing the built source
172@@ -201,7 +303,6 @@
173 # devportal-metadata.json) are tarred up and stored in a preconfigured
174 # location: PKGME_OUTPUT_DIRECTORY.
175 working_dirs = []
176- logger = self.get_logger()
177 with TempDir() as temp_dir:
178 output_dir = temp_dir.path
179 download_dir = os.path.join(
180@@ -256,97 +357,6 @@
181 return make_tarball(
182 output_dir, settings.PKGME_OUTPUT_DIRECTORY, package_name)
183
184- def _submit_error(self, metadata, error_data):
185- if metadata.get('errback_url', None):
186- self.get_logger().error(
187- "Submitting error info to %s" % (metadata['errback_url'],))
188- try:
189- submit_error(metadata, error_data, logger=self.get_logger())
190- except CallbackError, e:
191- # XXX: Should generate an OOPS report and log the OOPS number.
192- # Obviously we can't send to the client.
193- self.get_logger().error(
194- "Error while trying to submit error to client: %s\n%s"
195- % (str(e), error_data))
196- else:
197- raise
198-
199- def _validate_metadata(self, metadata):
200- """Make sure 'metadata' has all the fields we need."""
201- missing_fields = []
202- for field in self.REQUIRED_FIELDS:
203- if field not in metadata:
204- missing_fields.append(field)
205- if missing_fields:
206- raise MissingFieldsError(metadata, missing_fields)
207-
208- def _log_failure_exception(self, metadata, ftype):
209- self.get_logger().info("%s (%s) failed with %s exception: %s" % (
210- ftype,
211- metadata.get('myapps_id', '<unknown myapps id>'),
212- metadata.get('package_url', '<unknown package url>'),
213- traceback.format_exc(),
214- ))
215-
216- def run(self, metadata, overrides):
217- """Build a source package.
218-
219- Requires 'metadata' to have:
220- * callback_url
221- * package_url
222-
223- Uses these if they exist:
224- * errback_url
225- * myapps_id
226-
227- Assumes that the backend can determine a non-empty value for
228- 'package_name'.
229- """
230- self._validate_metadata(metadata)
231- self.get_logger().info(
232- "Building package for %s: %s" % (
233- metadata.get('myapps_id', '<unknown myapps id>'),
234- metadata.get('package_url', '<unknown package url>'),
235- ))
236- try:
237- packaged_app_path = self.build_package(metadata, overrides)
238- except self.EXPECTED_EXCEPTIONS, e:
239- # pkgme couldn't make the package. If the user were running
240- # 'pkgme' on the command line they would get a simple,
241- # user-friendly error. Just send that, and not the whole
242- # traceback.
243- self._log_failure_exception(metadata, "user")
244- self._submit_error(metadata, str(e))
245- except FATAL_EXCEPTIONS:
246- # Someone hit Ctrl-C or something like that. Just keep raising.
247- raise
248- except:
249- # The exception is something unexpected, and thus represents a bug
250- # in the system.
251- #
252- # XXX: Should generate an OOPS and report that to the client. For
253- # now, send the error across without the traceback. This is an
254- # interim measure to reduce our reliance on timeouts for doing
255- # system testing.
256- self._log_failure_exception(metadata, "internal")
257- exc_type, exc_obj, tb = sys.exc_info()
258- self._submit_error(
259- metadata, exc_info_as_dict((exc_type, exc_obj, None)))
260- else:
261- packaged_app_url = get_url_for_file(packaged_app_path)
262- self.get_logger().info(
263- "Submitting success info to %r: %s"
264- % (metadata['callback_url'], packaged_app_url))
265- try:
266- submit_pkgme_info(packaged_app_url, metadata,
267- logger=self.get_logger())
268- except CallbackError:
269- self._submit_error(metadata, exc_info_as_dict(sys.exc_info()))
270- else:
271- return packaged_app_url
272-
273-tasks.register(BuildPackageTask)
274-
275
276 class CauseOopsTask(Task):
277 def run(self):
278
279=== modified file 'src/djpkgme/tests/test_handlers.py'
280--- src/djpkgme/tests/test_handlers.py 2012-03-12 14:10:27 +0000
281+++ src/djpkgme/tests/test_handlers.py 2013-10-09 20:40:35 +0000
282@@ -46,7 +46,7 @@
283
284 # Patch httplib2 so the handler doesn't try to PUT to callback_url
285 @patch('httplib2.Http.request', return_value=(FakeResponse(200, 'OK'), ''))
286- @patch('djpkgme.tasks.BuildPackageTask.build_package')
287+ @patch('djpkgme.tasks.PackageAppJob.run')
288 def test_no_overrides_success(self, mock_build_package, mock_request):
289 request = Mock()
290 mock_build_package.return_value = 'foo'
291@@ -57,7 +57,7 @@
292
293 # Patch httplib2 so the handler doesn't try to PUT to callback_url
294 @patch('httplib2.Http.request', return_value=(FakeResponse(200, 'OK'), ''))
295- @patch('djpkgme.tasks.BuildPackageTask.build_package')
296+ @patch('djpkgme.tasks.PackageAppJob.run')
297 def test_overrides_success(self, mock_build_package, mock_request):
298 request = Mock()
299 mock_build_package.return_value = 'foo'
300
301=== modified file 'src/djpkgme/tests/test_tasks.py'
302--- src/djpkgme/tests/test_tasks.py 2012-11-15 15:34:28 +0000
303+++ src/djpkgme/tests/test_tasks.py 2013-10-09 20:40:35 +0000
304@@ -2,6 +2,7 @@
305 # http://ask.github.com/celery/cookbook/unit-testing.html
306
307 import json
308+import logging
309 import os
310 import sys
311 import tarfile
312@@ -32,6 +33,7 @@
313 BuildPackageTask,
314 CallbackError,
315 get_url_for_file,
316+ PackageAppJob,
317 prepare_icons,
318 prepare_metadata,
319 translate_dict,
320@@ -225,7 +227,7 @@
321 class BuildPackageTaskTestCase(TestCaseWithFactory):
322
323 @patch('httplib2.Http.request', return_value=(FakeResponse(200, 'OK'), ''))
324- @patch('djpkgme.tasks.BuildPackageTask.build_package')
325+ @patch('djpkgme.tasks.PackageAppJob.run')
326 def test_build_package_is_called(self, mock_build_package, mock_request):
327 mock_build_package.return_value = "foo"
328
329@@ -234,12 +236,11 @@
330
331 result = BuildPackageTask.delay(metadata, overrides)
332
333- self.assertEqual(get_url_for_file('foo'), result.get())
334 self.assertTrue(result.successful())
335 self.assertEqual(1, mock_build_package.call_count)
336
337 @patch('httplib2.Http.request', return_value=(FakeResponse(200, 'OK'), ''))
338- @patch('djpkgme.tasks.BuildPackageTask.build_package')
339+ @patch('djpkgme.tasks.PackageAppJob.run')
340 def test_result_is_submitted(self, mock_build_package, mock_request):
341 mock_build_package.return_value = "foo"
342
343@@ -260,8 +261,8 @@
344 self.assertEqual(expected, json.loads(kwargs['body']))
345
346 @patch('djpkgme.tasks.submit_error')
347- @patch('djpkgme.tasks.submit_pkgme_info')
348- @patch('djpkgme.tasks.BuildPackageTask.build_package')
349+ @patch('djpkgme.tasks.submit_to_myapps')
350+ @patch('djpkgme.tasks.PackageAppJob.run')
351 def test_cannot_send_callback(self, mock_build_package, mock_submit,
352 mock_error):
353 # If we get a negative response to our callback, then we try to send
354@@ -285,7 +286,7 @@
355
356 @patch('djpkgme.tasks.BuildPackageTask.get_logger')
357 @patch('djpkgme.tasks.submit_error')
358- @patch('djpkgme.tasks.BuildPackageTask.build_package')
359+ @patch('djpkgme.tasks.PackageAppJob.run')
360 def test_cannot_send_errback(self, mock_build_package, mock_error,
361 mock_get_logger):
362 # If we get a negative response to our errback, then we just log the
363@@ -310,25 +311,24 @@
364 self.patch(task.request, 'loglevel', UNREASONABLY_HIGH_LEVEL)
365
366 @patch('djpkgme.client.submit_to_myapps')
367- @patch('djpkgme.tasks.BuildPackageTask.build_package')
368+ @patch('djpkgme.tasks.PackageAppJob.run')
369 def test_failed_build_package(self, mock_build_package, mock_submit):
370 metadata = self.factory.make_metadata()
371 mock_build_package.side_effect = PkgmeError('some failure')
372 task = BuildPackageTask()
373 self._suppress_logging(task)
374 task.run(metadata, {})
375- [((url, json_body), kwargs)] = mock_submit.call_args_list
376+ [((url, body), kwargs)] = mock_submit.call_args_list
377 self.assertEqual(metadata['errback_url'], url)
378 del kwargs['logger']
379 self.assertEqual({}, kwargs)
380- body = json.loads(json_body)
381 error = body.pop('error')
382 self.assertEqual(
383 [metadata['errback_url'],
384 get_response_dict(None, metadata), 'some failure', {}],
385 [url, body, error, kwargs])
386
387- @patch('djpkgme.tasks.BuildPackageTask.build_package')
388+ @patch('djpkgme.tasks.PackageAppJob.run')
389 def test_failed_build_package_no_errback_url(self, mock_build_package):
390 # If no errback URL is provided then just raise.
391 metadata = self.factory.make_metadata()
392@@ -338,7 +338,7 @@
393 self.assertRaises(PkgmeError, task.run, metadata, {})
394
395 @patch('djpkgme.client.submit_to_myapps')
396- @patch('djpkgme.tasks.BuildPackageTask.build_package')
397+ @patch('djpkgme.tasks.PackageAppJob.run')
398 def test_failed_build_package_unexpected(self, mock_build_package,
399 mock_submit):
400 metadata = self.factory.make_metadata()
401@@ -347,11 +347,10 @@
402 task = BuildPackageTask()
403 self._suppress_logging(task)
404 task.run(metadata, {})
405- [((url, json_body), kwargs)] = mock_submit.call_args_list
406+ [((url, body), kwargs)] = mock_submit.call_args_list
407 self.assertEqual(metadata['errback_url'], url)
408 del kwargs['logger']
409 self.assertEqual({}, kwargs)
410- body = json.loads(json_body)
411 error = body.pop('error')
412 self.assertThat(
413 [url, body, error, kwargs],
414@@ -363,7 +362,7 @@
415 'traceback': 'RuntimeError: some failure\n'}),
416 Equals({})]))
417
418- @patch('djpkgme.tasks.BuildPackageTask.build_package')
419+ @patch('djpkgme.tasks.PackageAppJob.run')
420 def test_failed_build_package_unexpected_no_errback_url(
421 self, mock_build_package):
422 # If no errback URL is provided then just raise.
423@@ -373,7 +372,7 @@
424 task = BuildPackageTask()
425 self.assertRaises(RuntimeError, task.run, metadata, {})
426
427- @patch('djpkgme.tasks.BuildPackageTask.build_package')
428+ @patch('djpkgme.tasks.PackageAppJob.run')
429 def test_failed_build_package_fatal(self, mock_build_package):
430 # If keyboard interrupt or something like that is raised, then
431 # re-raise, even if errback_url is set.
432@@ -383,11 +382,11 @@
433 self.assertRaises(KeyboardInterrupt, task.run, metadata, {})
434
435
436-class TestBuildPackageTaskIntegration(TestCaseWithFactory):
437+class TestPackageAppJobIntegration(TestCaseWithFactory):
438 """Tests that actually build the package."""
439
440 def run_task(self, metadata, file_path):
441- task = BuildPackageTask()
442+ job = PackageAppJob()
443 def download_file(url, output_dir):
444 # Create a tarball and stick it in 'output_dir'.
445 proper_location = os.path.join(
446@@ -398,7 +397,8 @@
447 self.useFixture(LibdepFixture(DEPENDENCIES_FOR_TEST_DATA))
448 temp_dir = self.useFixture(TempDir()).path
449 self.patch(settings, 'PKGME_OUTPUT_DIRECTORY', temp_dir)
450- return task.build_package(metadata, {})
451+ logger = logging.getLogger()
452+ return job.run(metadata, {}, logger)
453
454 def test_writes_packaging_for_tarball(self):
455 # BuildPackageTask.build_package runs pkgme to build the package and

Subscribers

People subscribed via source and target branches