Merge lp:~lifeless/lptools/upstream into lp:~dobey/lptools/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
Reviewer Review Type Date Requested Status
dobey Approve
Elliot Murphy Pending
Review via email: mp+19764@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

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.

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
=== added file 'bin/lp-milestones'
--- bin/lp-milestones 1970-01-01 00:00:00 +0000
+++ bin/lp-milestones 2010-02-20 06:50:29 +0000
@@ -0,0 +1,189 @@
1#!/usr/bin/python
2#
3# Author: Robert Collins <robert.collins@canonical.com>
4#
5# Copyright 2010 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18
19from __future__ import with_statement
20import time
21import optparse
22import os
23import sys
24
25# Might want to make errors import lazy.
26from bzrlib import commands, errors, ui, version_info as bzr_version_info
27from launchpadlib.errors import HTTPError
28
29from lptools import config
30
31def list_commands(command_names):
32 mod = sys.modules[__name__]
33 command_names.update(commands._scan_module_for_commands(mod))
34 return command_names
35
36
37def get_command(cmd_or_None, cmd_name):
38 if cmd_name is None:
39 return cmd_help()
40 try:
41 return globals()['cmd_' + cmd_name]()
42 except KeyError:
43 return cmd_or_None
44
45
46class LaunchpadCommand(commands.Command):
47 """Base class for commands working with launchpad."""
48
49 def run_argv_aliases(self, argv, alias_argv=None):
50 # This might not be unique-enough for a cachedir; can do
51 # lp-milestones/cmdname if needed.
52 self.launchpad = config.get_launchpad('lp-milestones')
53 return commands.Command.run_argv_aliases(self, argv, alias_argv)
54
55
56class cmd_create(LaunchpadCommand):
57 """Create a milestone.
58
59 lp-milestone create projectname/seriesname/milestonename
60 """
61
62 takes_args = ['milestone']
63
64 def run(self, milestone):
65 components = milestone.split('/')
66 if len(components) != 3:
67 raise errors.BzrCommandError("milestone (%s) too short or too long."
68 % milestone)
69 projectname, seriesname, milestonename = components
70 # Direct access takes 50% of the time of doing traversal.
71 #proj = self.launchpad.projects[projectname]
72 #series = proj.getSeries(name=seriesname)
73 series = self.launchpad.load(projectname + '/' + seriesname)
74 milestone = series.newMilestone(name=milestonename)
75
76
77class cmd_delete(LaunchpadCommand):
78 """Delete a milestone.
79
80 lp-milestone delete projectname/milestonename
81
82 Note that this cannot delete a milestone with releases on it (yet). The
83 server will throw a 500 error, and you may see
84 File "/srv/////lib/lp/registry/model/milestone.py", line 209, in destroySelf
85 "You cannot delete a milestone which has a product release "
86 AssertionError: You cannot delete a milestone which has a product release associated with it.
87 In the trace if you have appropriate access.
88 """
89
90 takes_args = ['milestone']
91
92 def run(self, milestone):
93 components = milestone.split('/')
94 if len(components) != 2:
95 raise errors.BzrCommandError("milestone (%s) too short or too long."
96 % milestone)
97 m = self.launchpad.load('%s/+milestone/%s' % tuple(components))
98 try:
99 m.delete()
100 except HTTPError, e:
101 if e.response.status == 404:
102 pass
103 elif e.response.status == 500:
104 self.outf.write("Perhaps the milestone has been released?\n")
105 self.outf.write("If so you can undo this in the web UI.\n")
106 raise
107 else:
108 raise
109
110
111class cmd_help(commands.Command):
112 """Show help on a command or other topic."""
113
114 # Can't use the stock bzrlib help, because the help indices aren't quite
115 # generic enough.
116 takes_args = ['topic?']
117 def run(self, topic=None):
118 if topic is None:
119 self.outf.write(
120"""lp-milestones -- An lptools command to work with milestones in launchpad.
121https://launchpad.net/lptools/
122
123lp-milestones help commands -- list commands
124""")
125 else:
126 import bzrlib.help
127 bzrlib.help.help(topic)
128
129
130class cmd_release(LaunchpadCommand):
131 """Create a release from a milestone.
132
133 lp-milestone release projectname/milestonename
134 """
135
136 takes_args = ['milestone']
137
138 def run(self, milestone):
139 components = milestone.split('/')
140 if len(components) != 2:
141 raise errors.BzrCommandError("milestone (%s) too short or too long."
142 % milestone)
143 m = self.launchpad.load('%s/+milestone/%s' % tuple(components))
144 now = time.strftime('%Y-%m-%d-%X', time.gmtime())
145 # note: there is a bug with how the releases are created, don't be surprised
146 # if they are created 'X hours ago' where 'X' is the hour in UTC.
147 m.createProductRelease(date_released=now)
148
149
150class cmd_rename(LaunchpadCommand):
151 """Rename a milestone.
152
153 lp-milestone rename projectname/milestonename newname
154 """
155
156 takes_args = ['milestone', 'newname']
157
158 def run(self, milestone, newname):
159 components = milestone.split('/')
160 if len(components) != 2:
161 raise errors.BzrCommandError("milestone (%s) too short or too long."
162 % milestone)
163 if '/' in newname:
164 raise errors.BzrCommandError(
165 "milestones can only be renamed within a project.")
166 m = self.launchpad.load('%s/+milestone/%s' % tuple(components))
167 m.name = newname
168 m.lp_save()
169
170
171def do_run_bzr(argv):
172 if bzr_version_info > (2, 2, 0):
173 # in bzr 2.2 we can disable bzr plugins so bzr commands don't show
174 # up.
175 return commands.run_bzr(argv, lambda:None, lambda:None)
176 else:
177 return commands.run_bzr(argv)
178
179
180def main():
181 commands.Command.hooks.install_named_hook('list_commands', list_commands,
182 "list")
183 commands.Command.hooks.install_named_hook('get_command', get_command,
184 "get")
185 ui.ui_factory = ui.make_ui_for_terminal(sys.stdin, sys.stdout, sys.stderr)
186 sys.exit(commands.exception_to_return_code(do_run_bzr, sys.argv[1:]))
187
188if __name__ == "__main__":
189 main()
0190
=== modified file 'bin/lp-review-list'
--- bin/lp-review-list 2010-02-01 05:30:54 +0000
+++ bin/lp-review-list 2010-02-20 06:50:30 +0000
@@ -17,25 +17,21 @@
17# with this program. If not, see <http://www.gnu.org/licenses/>.17# with this program. If not, see <http://www.gnu.org/licenses/>.
1818
19from __future__ import with_statement19from __future__ import with_statement
20from ConfigParser import ConfigParser
21import os
22import re
23import subprocess
24import sys
25from threading import Thread
2026
21import pygtk27import pygtk
22pygtk.require('2.0')28pygtk.require('2.0')
23import gobject29import gobject
24import gtk30import gtk
25import pango31import pango
26
27from ConfigParser import ConfigParser
28import os
29import re
30import subprocess
31import sys
32
33from xdg.BaseDirectory import xdg_cache_home, xdg_config_home32from xdg.BaseDirectory import xdg_cache_home, xdg_config_home
3433
35from threading import Thread34from lptools import config
36
37from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT
38from launchpadlib.credentials import Credentials
3935
40VOTES = { "Approve" : "#00ff00",36VOTES = { "Approve" : "#00ff00",
41 "Needs Fixing" : "#993300",37 "Needs Fixing" : "#993300",
@@ -65,6 +61,7 @@
65 else:61 else:
66 self.projects = []62 self.projects = []
6763
64 # XXX: Not currently honoured - not sure if it ever worked.
68 if self.config.has_option("lptools", "server"):65 if self.config.has_option("lptools", "server"):
69 self.api_server = self.config.get("lptools", "server")66 self.api_server = self.config.get("lptools", "server")
70 else:67 else:
@@ -167,7 +164,7 @@
167 self.set_title("Pending Reviews")164 self.set_title("Pending Reviews")
168 self.set_default_size(320, 400)165 self.set_default_size(320, 400)
169 self.connect("destroy", lambda w: gtk.main_quit())166 self.connect("destroy", lambda w: gtk.main_quit())
170 self.connect("delete_event", lambda w: gtk.main_quit())167 self.connect("delete_event", lambda w,x: gtk.main_quit())
171168
172 vbox = gtk.VBox()169 vbox = gtk.VBox()
173 self.add(vbox)170 self.add(vbox)
@@ -203,10 +200,6 @@
203 col = gtk.TreeViewColumn("Branch", cell, markup=0)200 col = gtk.TreeViewColumn("Branch", cell, markup=0)
204 view.append_column(col)201 view.append_column(col)
205202
206 self.cachedir = os.path.join(xdg_cache_home, "review-list")
207 if not os.path.isdir(self.cachedir):
208 os.makedirs(self.cachedir)
209
210 self.launchpad = None203 self.launchpad = None
211 self.me = None204 self.me = None
212205
@@ -220,21 +213,7 @@
220 Thread(target=self.__lp_login).start()213 Thread(target=self.__lp_login).start()
221214
222 def __lp_login(self):215 def __lp_login(self):
223 credsfile = os.path.join(self.cachedir, "credentials")216 self.launchpad = config.get_launchpad("review-list")
224
225 if os.path.exists(credsfile):
226 creds = Credentials()
227
228 with file(credsfile) as f:
229 creds.load(f)
230 self.launchpad = Launchpad(creds, EDGE_SERVICE_ROOT)
231 else:
232 self.launchpad = Launchpad.get_token_and_login(
233 'review-list',
234 EDGE_SERVICE_ROOT,
235 self.cachedir)
236 with file(credsfile, "w") as f:
237 self.launchpad.credentials.save(f)
238217
239 self.me = self.launchpad.me218 self.me = self.launchpad.me
240219
241220
=== modified file 'bin/lp-review-notifier'
--- bin/lp-review-notifier 2010-02-01 05:30:54 +0000
+++ bin/lp-review-notifier 2010-02-20 06:50:29 +0000
@@ -17,21 +17,18 @@
17# with this program. If not, see <http://www.gnu.org/licenses/>.17# with this program. If not, see <http://www.gnu.org/licenses/>.
1818
19from __future__ import with_statement19from __future__ import with_statement
20import os
21import sys
2022
21import pygtk
22pygtk.require('2.0')
23import gobject23import gobject
24import gtk24import gtk
2525import pygtk
26import pynotify26import pynotify
27
28import os
29import sys
30
31from xdg.BaseDirectory import xdg_cache_home27from xdg.BaseDirectory import xdg_cache_home
3228
33from launchpadlib.launchpad import Launchpad, EDGE_SERVICE_ROOT29from lptools import config
34from launchpadlib.credentials import Credentials30
31pygtk.require('2.0')
3532
36ICON_NAME = "bzr-icon-64"33ICON_NAME = "bzr-icon-64"
3734
@@ -40,31 +37,14 @@
40 def __init__(self):37 def __init__(self):
41 self.id = 038 self.id = 0
42 self.cached_candidates = {}39 self.cached_candidates = {}
4340 self.launchpad = config.get_launchpad("review-notifier")
44 self.cachedir = os.path.join(xdg_cache_home, "review-notifier")
45 credsfile = os.path.join(self.cachedir, "credentials")
46
47 if not os.path.isdir(self.cachedir):
48 os.makedirs(self.cachedir)
49
50 if os.path.exists(credsfile):
51 creds = Credentials()
52 with file(credsfile) as f:
53 creds.load(f)
54 self.launchpad = Launchpad(creds, EDGE_SERVICE_ROOT)
55 else:
56 self.launchpad = Launchpad.get_token_and_login('review-notifier',
57 EDGE_SERVICE_ROOT,
58 self.cachedir)
59 with file(credsfile, "w") as f:
60 self.launchpad.credentials.save(f)
61
62 self.me = self.launchpad.me41 self.me = self.launchpad.me
6342
64 print "Allo, %s" % self.me.name43 print "Allo, %s" % self.me.name
6544
66 self.projects = []45 self.projects = []
6746
47 # TODO: get the config from lptools.conf
68 for arg in sys.argv:48 for arg in sys.argv:
69 if not arg.endswith("review-notifier"):49 if not arg.endswith("review-notifier"):
70 self.projects.append(arg)50 self.projects.append(arg)
7151
=== added directory 'lptools'
=== added file 'lptools/__init__.py'
--- lptools/__init__.py 1970-01-01 00:00:00 +0000
+++ lptools/__init__.py 2010-02-20 06:50:30 +0000
@@ -0,0 +1,20 @@
1# Author: Robert Collins <robert.collins@canonical.com>
2#
3# Copyright 2010 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""LP-tools is largely command line tools. The package contains various helpers.
18
19See lptools.config for configuration support logic.
20"""
021
=== added file 'lptools/config.py'
--- lptools/config.py 1970-01-01 00:00:00 +0000
+++ lptools/config.py 2010-02-20 06:50:30 +0000
@@ -0,0 +1,83 @@
1# Author: Robert Collins <robert.collins@canonical.com>
2#
3# Copyright 2010 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16
17from __future__ import with_statement
18
19"""Configuration glue for lptools."""
20
21__all__ = [
22 "ensure_dir",
23 "get_launchpad",
24 "lptools_cachedir",
25 "lptools_credentials_path",
26 ]
27
28import os.path
29
30from launchpadlib.credentials import Credentials
31from launchpadlib.launchpad import EDGE_SERVICE_ROOT
32from xdg.BaseDirectory import xdg_cache_home
33
34from lptools import launchpad
35
36
37def ensure_dir(dir):
38 """Ensure that dir exists."""
39 if not os.path.isdir(dir):
40 os.makedirs(dir)
41
42
43def get_launchpad(appname):
44 """Get a login to launchpad for lptools caching in cachedir.
45
46 Note that caching is not multiple-process safe in launchpadlib, and the
47 appname parameter is used to create per-app cachedirs.
48
49 :param appname: The name of the app used to create per-app cachedirs.
50 """
51 cachedir = os.path.join(xdg_cache_home, appname)
52 ensure_dir(cachedir)
53 credspath = lptools_credentials_path()
54 if os.path.exists(credspath):
55 creds = Credentials()
56 with file(credspath) as f:
57 creds.load(f)
58 return launchpad.Launchpad(creds, EDGE_SERVICE_ROOT, cachedir)
59 else:
60 result = launchpad.Launchpad.get_token_and_login('lptools',
61 EDGE_SERVICE_ROOT, cachedir)
62 with file(credspath, "w") as f:
63 result.credentials.save(f)
64 return result
65
66
67def lptools_cachedir():
68 """Return the cachedir for common lptools things.
69
70 This is xdg_cache_home/lptools.
71 """
72 return os.path.join(xdg_cache_home, "lptools")
73
74
75def lptools_credentials_path():
76 """Return the path to the lptools credentials file.
77
78 This also ensures the path is usable by checking it's containing directory
79 exists.
80 """
81 cachedir = lptools_cachedir()
82 ensure_dir(cachedir)
83 return os.path.join(lptools_cachedir(), 'credentials')
084
=== added file 'lptools/launchpad.py'
--- lptools/launchpad.py 1970-01-01 00:00:00 +0000
+++ lptools/launchpad.py 2010-02-20 06:50:30 +0000
@@ -0,0 +1,38 @@
1# Author: Robert Collins <robert.collins@canonical.com>
2#
3# Copyright 2010 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16
17__all__ = [
18 'Launchpad',
19 ]
20
21"""Wrapper class for launchpadlib with tweaks."""
22
23from launchpadlib.launchpad import Launchpad as UpstreamLaunchpad
24
25class Launchpad(UpstreamLaunchpad):
26 """Launchpad object with bugfixes."""
27
28 def load(self, url_string):
29 """Load an object.
30
31 Extended to support url_string being a relative url.
32
33 Needed until bug 524775 is fixed.
34 """
35 if not url_string.startswith('https:'):
36 return UpstreamLaunchpad.load(self, str(self._root_uri) + url_string)
37 else:
38 return UpstreamLaunchpad.load(self, url_string)
039
=== modified file 'setup.py'
--- setup.py 2010-02-01 05:30:54 +0000
+++ setup.py 2010-02-20 06:50:29 +0000
@@ -2,6 +2,9 @@
22
3from glob import glob3from glob import glob
4from distutils.core import setup4from distutils.core import setup
5import os.path
6
7description = file(os.path.join(os.path.dirname(__file__), 'README'), 'rb').read()
58
6setup(9setup(
7 name='lptools',10 name='lptools',
@@ -11,7 +14,17 @@
11 author_email='rodney.dawes@canonical.com',14 author_email='rodney.dawes@canonical.com',
12 license='GPLv3',15 license='GPLv3',
13 description='A collection of tools for developers who use launchpad',16 description='A collection of tools for developers who use launchpad',
17 long_description=description,
14 py_modules=[],18 py_modules=[],
19 packages=['lptools'],
15 scripts=glob('bin/*'),20 scripts=glob('bin/*'),
21 classifiers = [
22 'Development Status :: 4 - Beta',
23 'Intended Audience :: Developers',
24 'License :: OSI Approved :: GNU General Public License v3 (GPL3)'
25 'Operating System :: OS Independent',
26 'Programming Language :: Python',
27 'Topic :: Software Development',
28 ],
16 )29 )
1730

Subscribers

People subscribed via source and target branches

to all changes: