Merge lp:~lifeless/lptools/upstream into lp:~dobey/lptools/trunk
- upstream
- Merge into trunk
Proposed by
Robert Collins
Status: | Merged |
---|---|
Approved by: | dobey |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~lifeless/lptools/upstream |
Merge into: | lp:~dobey/lptools/trunk |
Diff against target: |
535 lines (+361/-59) 7 files modified
bin/lp-milestones (+189/-0) bin/lp-review-list (+10/-31) bin/lp-review-notifier (+8/-28) lptools/__init__.py (+20/-0) lptools/config.py (+83/-0) lptools/launchpad.py (+38/-0) setup.py (+13/-0) |
To merge this branch: | bzr merge lp:~lifeless/lptools/upstream |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
dobey | Approve | ||
Elliot Murphy | Pending | ||
Review via email: mp+19764@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote : | # |
Revision history for this message
Robert Collins (lifeless) wrote : | # |
Oh, and commandant depends on bzrlib anyhow :)
Revision history for this message
Robert Collins (lifeless) wrote : | # |
I've added a delete and rename command to this.
lp:~lifeless/lptools/upstream
updated
- 14. By Robert Collins
-
Faster creation please.
- 15. By Robert Collins
-
Work around launchpadlib wanting absolute urls for.load.
- 16. By Robert Collins
-
Add milestone deletion.
- 17. By Robert Collins
-
Add milestone renaming.
Revision history for this message
Robert Collins (lifeless) wrote : | # |
I've also added 'release' as a subcommand, so one can totally script the release process.
lp:~lifeless/lptools/upstream
updated
- 18. By Robert Collins
-
Add releasing.
- 19. By Robert Collins
-
Correctly handle failures in milestones delete.
Revision history for this message
dobey (dobey) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'bin/lp-milestones' |
2 | --- bin/lp-milestones 1970-01-01 00:00:00 +0000 |
3 | +++ bin/lp-milestones 2010-02-20 06:50:29 +0000 |
4 | @@ -0,0 +1,189 @@ |
5 | +#!/usr/bin/python |
6 | +# |
7 | +# Author: Robert Collins <robert.collins@canonical.com> |
8 | +# |
9 | +# Copyright 2010 Canonical Ltd. |
10 | +# |
11 | +# This program is free software: you can redistribute it and/or modify it |
12 | +# under the terms of the GNU General Public License version 3, as published |
13 | +# by the Free Software Foundation. |
14 | +# |
15 | +# This program is distributed in the hope that it will be useful, but |
16 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
17 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
18 | +# PURPOSE. See the GNU General Public License for more details. |
19 | +# |
20 | +# You should have received a copy of the GNU General Public License along |
21 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
22 | + |
23 | +from __future__ import with_statement |
24 | +import time |
25 | +import optparse |
26 | +import os |
27 | +import sys |
28 | + |
29 | +# Might want to make errors import lazy. |
30 | +from bzrlib import commands, errors, ui, version_info as bzr_version_info |
31 | +from launchpadlib.errors import HTTPError |
32 | + |
33 | +from lptools import config |
34 | + |
35 | +def list_commands(command_names): |
36 | + mod = sys.modules[__name__] |
37 | + command_names.update(commands._scan_module_for_commands(mod)) |
38 | + return command_names |
39 | + |
40 | + |
41 | +def get_command(cmd_or_None, cmd_name): |
42 | + if cmd_name is None: |
43 | + return cmd_help() |
44 | + try: |
45 | + return globals()['cmd_' + cmd_name]() |
46 | + except KeyError: |
47 | + return cmd_or_None |
48 | + |
49 | + |
50 | +class LaunchpadCommand(commands.Command): |
51 | + """Base class for commands working with launchpad.""" |
52 | + |
53 | + def run_argv_aliases(self, argv, alias_argv=None): |
54 | + # This might not be unique-enough for a cachedir; can do |
55 | + # lp-milestones/cmdname if needed. |
56 | + self.launchpad = config.get_launchpad('lp-milestones') |
57 | + return commands.Command.run_argv_aliases(self, argv, alias_argv) |
58 | + |
59 | + |
60 | +class cmd_create(LaunchpadCommand): |
61 | + """Create a milestone. |
62 | + |
63 | + lp-milestone create projectname/seriesname/milestonename |
64 | + """ |
65 | + |
66 | + takes_args = ['milestone'] |
67 | + |
68 | + def run(self, milestone): |
69 | + components = milestone.split('/') |
70 | + if len(components) != 3: |
71 | + raise errors.BzrCommandError("milestone (%s) too short or too long." |
72 | + % milestone) |
73 | + projectname, seriesname, milestonename = components |
74 | + # Direct access takes 50% of the time of doing traversal. |
75 | + #proj = self.launchpad.projects[projectname] |
76 | + #series = proj.getSeries(name=seriesname) |
77 | + series = self.launchpad.load(projectname + '/' + seriesname) |
78 | + milestone = series.newMilestone(name=milestonename) |
79 | + |
80 | + |
81 | +class cmd_delete(LaunchpadCommand): |
82 | + """Delete a milestone. |
83 | + |
84 | + lp-milestone delete projectname/milestonename |
85 | + |
86 | + Note that this cannot delete a milestone with releases on it (yet). The |
87 | + server will throw a 500 error, and you may see |
88 | + File "/srv/////lib/lp/registry/model/milestone.py", line 209, in destroySelf |
89 | + "You cannot delete a milestone which has a product release " |
90 | + AssertionError: You cannot delete a milestone which has a product release associated with it. |
91 | + In the trace if you have appropriate access. |
92 | + """ |
93 | + |
94 | + takes_args = ['milestone'] |
95 | + |
96 | + def run(self, milestone): |
97 | + components = milestone.split('/') |
98 | + if len(components) != 2: |
99 | + raise errors.BzrCommandError("milestone (%s) too short or too long." |
100 | + % milestone) |
101 | + m = self.launchpad.load('%s/+milestone/%s' % tuple(components)) |
102 | + try: |
103 | + m.delete() |
104 | + except HTTPError, e: |
105 | + if e.response.status == 404: |
106 | + pass |
107 | + elif e.response.status == 500: |
108 | + self.outf.write("Perhaps the milestone has been released?\n") |
109 | + self.outf.write("If so you can undo this in the web UI.\n") |
110 | + raise |
111 | + else: |
112 | + raise |
113 | + |
114 | + |
115 | +class cmd_help(commands.Command): |
116 | + """Show help on a command or other topic.""" |
117 | + |
118 | + # Can't use the stock bzrlib help, because the help indices aren't quite |
119 | + # generic enough. |
120 | + takes_args = ['topic?'] |
121 | + def run(self, topic=None): |
122 | + if topic is None: |
123 | + self.outf.write( |
124 | +"""lp-milestones -- An lptools command to work with milestones in launchpad. |
125 | +https://launchpad.net/lptools/ |
126 | + |
127 | +lp-milestones help commands -- list commands |
128 | +""") |
129 | + else: |
130 | + import bzrlib.help |
131 | + bzrlib.help.help(topic) |
132 | + |
133 | + |
134 | +class cmd_release(LaunchpadCommand): |
135 | + """Create a release from a milestone. |
136 | + |
137 | + lp-milestone release projectname/milestonename |
138 | + """ |
139 | + |
140 | + takes_args = ['milestone'] |
141 | + |
142 | + def run(self, milestone): |
143 | + components = milestone.split('/') |
144 | + if len(components) != 2: |
145 | + raise errors.BzrCommandError("milestone (%s) too short or too long." |
146 | + % milestone) |
147 | + m = self.launchpad.load('%s/+milestone/%s' % tuple(components)) |
148 | + now = time.strftime('%Y-%m-%d-%X', time.gmtime()) |
149 | + # note: there is a bug with how the releases are created, don't be surprised |
150 | + # if they are created 'X hours ago' where 'X' is the hour in UTC. |
151 | + m.createProductRelease(date_released=now) |
152 | + |
153 | + |
154 | +class cmd_rename(LaunchpadCommand): |
155 | + """Rename a milestone. |
156 | + |
157 | + lp-milestone rename projectname/milestonename newname |
158 | + """ |
159 | + |
160 | + takes_args = ['milestone', 'newname'] |
161 | + |
162 | + def run(self, milestone, newname): |
163 | + components = milestone.split('/') |
164 | + if len(components) != 2: |
165 | + raise errors.BzrCommandError("milestone (%s) too short or too long." |
166 | + % milestone) |
167 | + if '/' in newname: |
168 | + raise errors.BzrCommandError( |
169 | + "milestones can only be renamed within a project.") |
170 | + m = self.launchpad.load('%s/+milestone/%s' % tuple(components)) |
171 | + m.name = newname |
172 | + m.lp_save() |
173 | + |
174 | + |
175 | +def do_run_bzr(argv): |
176 | + if bzr_version_info > (2, 2, 0): |
177 | + # in bzr 2.2 we can disable bzr plugins so bzr commands don't show |
178 | + # up. |
179 | + return commands.run_bzr(argv, lambda:None, lambda:None) |
180 | + else: |
181 | + return commands.run_bzr(argv) |
182 | + |
183 | + |
184 | +def main(): |
185 | + commands.Command.hooks.install_named_hook('list_commands', list_commands, |
186 | + "list") |
187 | + commands.Command.hooks.install_named_hook('get_command', get_command, |
188 | + "get") |
189 | + ui.ui_factory = ui.make_ui_for_terminal(sys.stdin, sys.stdout, sys.stderr) |
190 | + sys.exit(commands.exception_to_return_code(do_run_bzr, sys.argv[1:])) |
191 | + |
192 | +if __name__ == "__main__": |
193 | + main() |
194 | |
195 | === modified file 'bin/lp-review-list' |
196 | --- bin/lp-review-list 2010-02-01 05:30:54 +0000 |
197 | +++ bin/lp-review-list 2010-02-20 06:50:30 +0000 |
198 | @@ -17,25 +17,21 @@ |
199 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
200 | |
201 | from __future__ import with_statement |
202 | +from ConfigParser import ConfigParser |
203 | +import os |
204 | +import re |
205 | +import subprocess |
206 | +import sys |
207 | +from threading import Thread |
208 | |
209 | import pygtk |
210 | pygtk.require('2.0') |
211 | import gobject |
212 | import gtk |
213 | import pango |
214 | - |
215 | -from ConfigParser import ConfigParser |
216 | -import os |
217 | -import re |
218 | -import subprocess |
219 | -import sys |
220 | - |
221 | from xdg.BaseDirectory import xdg_cache_home, xdg_config_home |
222 | |
223 | -from threading import Thread |
224 | - |
225 | -from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT |
226 | -from launchpadlib.credentials import Credentials |
227 | +from lptools import config |
228 | |
229 | VOTES = { "Approve" : "#00ff00", |
230 | "Needs Fixing" : "#993300", |
231 | @@ -65,6 +61,7 @@ |
232 | else: |
233 | self.projects = [] |
234 | |
235 | + # XXX: Not currently honoured - not sure if it ever worked. |
236 | if self.config.has_option("lptools", "server"): |
237 | self.api_server = self.config.get("lptools", "server") |
238 | else: |
239 | @@ -167,7 +164,7 @@ |
240 | self.set_title("Pending Reviews") |
241 | self.set_default_size(320, 400) |
242 | self.connect("destroy", lambda w: gtk.main_quit()) |
243 | - self.connect("delete_event", lambda w: gtk.main_quit()) |
244 | + self.connect("delete_event", lambda w,x: gtk.main_quit()) |
245 | |
246 | vbox = gtk.VBox() |
247 | self.add(vbox) |
248 | @@ -203,10 +200,6 @@ |
249 | col = gtk.TreeViewColumn("Branch", cell, markup=0) |
250 | view.append_column(col) |
251 | |
252 | - self.cachedir = os.path.join(xdg_cache_home, "review-list") |
253 | - if not os.path.isdir(self.cachedir): |
254 | - os.makedirs(self.cachedir) |
255 | - |
256 | self.launchpad = None |
257 | self.me = None |
258 | |
259 | @@ -220,21 +213,7 @@ |
260 | Thread(target=self.__lp_login).start() |
261 | |
262 | def __lp_login(self): |
263 | - credsfile = os.path.join(self.cachedir, "credentials") |
264 | - |
265 | - if os.path.exists(credsfile): |
266 | - creds = Credentials() |
267 | - |
268 | - with file(credsfile) as f: |
269 | - creds.load(f) |
270 | - self.launchpad = Launchpad(creds, EDGE_SERVICE_ROOT) |
271 | - else: |
272 | - self.launchpad = Launchpad.get_token_and_login( |
273 | - 'review-list', |
274 | - EDGE_SERVICE_ROOT, |
275 | - self.cachedir) |
276 | - with file(credsfile, "w") as f: |
277 | - self.launchpad.credentials.save(f) |
278 | + self.launchpad = config.get_launchpad("review-list") |
279 | |
280 | self.me = self.launchpad.me |
281 | |
282 | |
283 | === modified file 'bin/lp-review-notifier' |
284 | --- bin/lp-review-notifier 2010-02-01 05:30:54 +0000 |
285 | +++ bin/lp-review-notifier 2010-02-20 06:50:29 +0000 |
286 | @@ -17,21 +17,18 @@ |
287 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
288 | |
289 | from __future__ import with_statement |
290 | +import os |
291 | +import sys |
292 | |
293 | -import pygtk |
294 | -pygtk.require('2.0') |
295 | import gobject |
296 | import gtk |
297 | - |
298 | +import pygtk |
299 | import pynotify |
300 | - |
301 | -import os |
302 | -import sys |
303 | - |
304 | from xdg.BaseDirectory import xdg_cache_home |
305 | |
306 | -from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT |
307 | -from launchpadlib.credentials import Credentials |
308 | +from lptools import config |
309 | + |
310 | +pygtk.require('2.0') |
311 | |
312 | ICON_NAME = "bzr-icon-64" |
313 | |
314 | @@ -40,31 +37,14 @@ |
315 | def __init__(self): |
316 | self.id = 0 |
317 | self.cached_candidates = {} |
318 | - |
319 | - self.cachedir = os.path.join(xdg_cache_home, "review-notifier") |
320 | - credsfile = os.path.join(self.cachedir, "credentials") |
321 | - |
322 | - if not os.path.isdir(self.cachedir): |
323 | - os.makedirs(self.cachedir) |
324 | - |
325 | - if os.path.exists(credsfile): |
326 | - creds = Credentials() |
327 | - with file(credsfile) as f: |
328 | - creds.load(f) |
329 | - self.launchpad = Launchpad(creds, EDGE_SERVICE_ROOT) |
330 | - else: |
331 | - self.launchpad = Launchpad.get_token_and_login('review-notifier', |
332 | - EDGE_SERVICE_ROOT, |
333 | - self.cachedir) |
334 | - with file(credsfile, "w") as f: |
335 | - self.launchpad.credentials.save(f) |
336 | - |
337 | + self.launchpad = config.get_launchpad("review-notifier") |
338 | self.me = self.launchpad.me |
339 | |
340 | print "Allo, %s" % self.me.name |
341 | |
342 | self.projects = [] |
343 | |
344 | + # TODO: get the config from lptools.conf |
345 | for arg in sys.argv: |
346 | if not arg.endswith("review-notifier"): |
347 | self.projects.append(arg) |
348 | |
349 | === added directory 'lptools' |
350 | === added file 'lptools/__init__.py' |
351 | --- lptools/__init__.py 1970-01-01 00:00:00 +0000 |
352 | +++ lptools/__init__.py 2010-02-20 06:50:30 +0000 |
353 | @@ -0,0 +1,20 @@ |
354 | +# Author: Robert Collins <robert.collins@canonical.com> |
355 | +# |
356 | +# Copyright 2010 Canonical Ltd. |
357 | +# |
358 | +# This program is free software: you can redistribute it and/or modify it |
359 | +# under the terms of the GNU General Public License version 3, as published |
360 | +# by the Free Software Foundation. |
361 | +# |
362 | +# This program is distributed in the hope that it will be useful, but |
363 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
364 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
365 | +# PURPOSE. See the GNU General Public License for more details. |
366 | +# |
367 | +# You should have received a copy of the GNU General Public License along |
368 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
369 | + |
370 | +"""LP-tools is largely command line tools. The package contains various helpers. |
371 | + |
372 | +See lptools.config for configuration support logic. |
373 | +""" |
374 | |
375 | === added file 'lptools/config.py' |
376 | --- lptools/config.py 1970-01-01 00:00:00 +0000 |
377 | +++ lptools/config.py 2010-02-20 06:50:30 +0000 |
378 | @@ -0,0 +1,83 @@ |
379 | +# Author: Robert Collins <robert.collins@canonical.com> |
380 | +# |
381 | +# Copyright 2010 Canonical Ltd. |
382 | +# |
383 | +# This program is free software: you can redistribute it and/or modify it |
384 | +# under the terms of the GNU General Public License version 3, as published |
385 | +# by the Free Software Foundation. |
386 | +# |
387 | +# This program is distributed in the hope that it will be useful, but |
388 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
389 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
390 | +# PURPOSE. See the GNU General Public License for more details. |
391 | +# |
392 | +# You should have received a copy of the GNU General Public License along |
393 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
394 | + |
395 | +from __future__ import with_statement |
396 | + |
397 | +"""Configuration glue for lptools.""" |
398 | + |
399 | +__all__ = [ |
400 | + "ensure_dir", |
401 | + "get_launchpad", |
402 | + "lptools_cachedir", |
403 | + "lptools_credentials_path", |
404 | + ] |
405 | + |
406 | +import os.path |
407 | + |
408 | +from launchpadlib.credentials import Credentials |
409 | +from launchpadlib.launchpad import EDGE_SERVICE_ROOT |
410 | +from xdg.BaseDirectory import xdg_cache_home |
411 | + |
412 | +from lptools import launchpad |
413 | + |
414 | + |
415 | +def ensure_dir(dir): |
416 | + """Ensure that dir exists.""" |
417 | + if not os.path.isdir(dir): |
418 | + os.makedirs(dir) |
419 | + |
420 | + |
421 | +def get_launchpad(appname): |
422 | + """Get a login to launchpad for lptools caching in cachedir. |
423 | + |
424 | + Note that caching is not multiple-process safe in launchpadlib, and the |
425 | + appname parameter is used to create per-app cachedirs. |
426 | + |
427 | + :param appname: The name of the app used to create per-app cachedirs. |
428 | + """ |
429 | + cachedir = os.path.join(xdg_cache_home, appname) |
430 | + ensure_dir(cachedir) |
431 | + credspath = lptools_credentials_path() |
432 | + if os.path.exists(credspath): |
433 | + creds = Credentials() |
434 | + with file(credspath) as f: |
435 | + creds.load(f) |
436 | + return launchpad.Launchpad(creds, EDGE_SERVICE_ROOT, cachedir) |
437 | + else: |
438 | + result = launchpad.Launchpad.get_token_and_login('lptools', |
439 | + EDGE_SERVICE_ROOT, cachedir) |
440 | + with file(credspath, "w") as f: |
441 | + result.credentials.save(f) |
442 | + return result |
443 | + |
444 | + |
445 | +def lptools_cachedir(): |
446 | + """Return the cachedir for common lptools things. |
447 | + |
448 | + This is xdg_cache_home/lptools. |
449 | + """ |
450 | + return os.path.join(xdg_cache_home, "lptools") |
451 | + |
452 | + |
453 | +def lptools_credentials_path(): |
454 | + """Return the path to the lptools credentials file. |
455 | + |
456 | + This also ensures the path is usable by checking it's containing directory |
457 | + exists. |
458 | + """ |
459 | + cachedir = lptools_cachedir() |
460 | + ensure_dir(cachedir) |
461 | + return os.path.join(lptools_cachedir(), 'credentials') |
462 | |
463 | === added file 'lptools/launchpad.py' |
464 | --- lptools/launchpad.py 1970-01-01 00:00:00 +0000 |
465 | +++ lptools/launchpad.py 2010-02-20 06:50:30 +0000 |
466 | @@ -0,0 +1,38 @@ |
467 | +# Author: Robert Collins <robert.collins@canonical.com> |
468 | +# |
469 | +# Copyright 2010 Canonical Ltd. |
470 | +# |
471 | +# This program is free software: you can redistribute it and/or modify it |
472 | +# under the terms of the GNU General Public License version 3, as published |
473 | +# by the Free Software Foundation. |
474 | +# |
475 | +# This program is distributed in the hope that it will be useful, but |
476 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
477 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
478 | +# PURPOSE. See the GNU General Public License for more details. |
479 | +# |
480 | +# You should have received a copy of the GNU General Public License along |
481 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
482 | + |
483 | +__all__ = [ |
484 | + 'Launchpad', |
485 | + ] |
486 | + |
487 | +"""Wrapper class for launchpadlib with tweaks.""" |
488 | + |
489 | +from launchpadlib.launchpad import Launchpad as UpstreamLaunchpad |
490 | + |
491 | +class Launchpad(UpstreamLaunchpad): |
492 | + """Launchpad object with bugfixes.""" |
493 | + |
494 | + def load(self, url_string): |
495 | + """Load an object. |
496 | + |
497 | + Extended to support url_string being a relative url. |
498 | + |
499 | + Needed until bug 524775 is fixed. |
500 | + """ |
501 | + if not url_string.startswith('https:'): |
502 | + return UpstreamLaunchpad.load(self, str(self._root_uri) + url_string) |
503 | + else: |
504 | + return UpstreamLaunchpad.load(self, url_string) |
505 | |
506 | === modified file 'setup.py' |
507 | --- setup.py 2010-02-01 05:30:54 +0000 |
508 | +++ setup.py 2010-02-20 06:50:29 +0000 |
509 | @@ -2,6 +2,9 @@ |
510 | |
511 | from glob import glob |
512 | from distutils.core import setup |
513 | +import os.path |
514 | + |
515 | +description = file(os.path.join(os.path.dirname(__file__), 'README'), 'rb').read() |
516 | |
517 | setup( |
518 | name='lptools', |
519 | @@ -11,7 +14,17 @@ |
520 | author_email='rodney.dawes@canonical.com', |
521 | license='GPLv3', |
522 | description='A collection of tools for developers who use launchpad', |
523 | + long_description=description, |
524 | py_modules=[], |
525 | + packages=['lptools'], |
526 | scripts=glob('bin/*'), |
527 | + classifiers = [ |
528 | + 'Development Status :: 4 - Beta', |
529 | + 'Intended Audience :: Developers', |
530 | + 'License :: OSI Approved :: GNU General Public License v3 (GPL3)' |
531 | + 'Operating System :: OS Independent', |
532 | + 'Programming Language :: Python', |
533 | + 'Topic :: Software Development', |
534 | + ], |
535 | ) |
536 |
This adds an lptools package and reduces some duplicate code; I also added a lp-milestones command with a 'create' command. I'll probably add a few more as I automate my release management process.
Oh, it uses bzrlib's command processing facilities, because they are awesome. You might say 'use commandant instead', but that isn't packaged yet.