~bzr-pqm/bzr/bzr.dev

5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
1
# Copyright (C) 2011 Canonical Ltd
5050.79.1 by John Arbash Meinel
Bring Maxb's code for querying launchpads api via a REST request.
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
"""Tools for dealing with the Launchpad API without using launchpadlib.
18
19
The api itself is a RESTful interface, so we can make HTTP queries directly.
20
loading launchpadlib itself has a fairly high overhead (just calling
21
Launchpad.login_anonymously() takes a 500ms once the WADL is cached, and 5+s to
22
get the WADL.
23
"""
24
25
try:
26
    # Use simplejson if available, much faster, and can be easily installed in
27
    # older versions of python
28
    import simplejson as json
29
except ImportError:
30
    # Is present since python 2.6
31
    try:
32
        import json
33
    except ImportError:
34
        json = None
35
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
36
import time
5050.79.1 by John Arbash Meinel
Bring Maxb's code for querying launchpads api via a REST request.
37
import urllib
38
import urllib2
39
6024.3.5 by John Arbash Meinel
Pull out code into helper functions, which allows us to test it.
40
from bzrlib import (
6024.3.7 by John Arbash Meinel
Add code to determine the moste recent tag.
41
    revision,
6024.3.5 by John Arbash Meinel
Pull out code into helper functions, which allows us to test it.
42
    trace,
43
    )
5050.79.1 by John Arbash Meinel
Bring Maxb's code for querying launchpads api via a REST request.
44
45
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
46
class LatestPublication(object):
47
    """Encapsulate how to find the latest publication for a given project."""
48
49
    LP_API_ROOT = 'https://api.launchpad.net/1.0'
50
51
    def __init__(self, archive, series, project):
52
        self._archive = archive
53
        self._project = project
54
        self._setup_series_and_pocket(series)
55
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
56
    def _setup_series_and_pocket(self, series):
57
        """Parse the 'series' info into a series and a pocket.
58
59
        eg::
60
            _setup_series_and_pocket('natty-proposed')
61
            => _series == 'natty'
62
               _pocket == 'Proposed'
63
        """
64
        self._series = series
65
        self._pocket = None
66
        if self._series is not None and '-' in self._series:
67
            self._series, self._pocket = self._series.split('-', 1)
68
            self._pocket = self._pocket.title()
5050.79.8 by John Arbash Meinel
We should supply pocket=Release when none is supplied.
69
        else:
70
            self._pocket = 'Release'
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
71
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
72
    def _archive_URL(self):
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
73
        """Return the Launchpad 'Archive' URL that we will query.
74
        This is everything in the URL except the query parameters.
75
        """
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
76
        return '%s/%s/+archive/primary' % (self.LP_API_ROOT, self._archive)
77
78
    def _publication_status(self):
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
79
        """Handle the 'status' field.
80
        It seems that Launchpad tracks all 'debian' packages as 'Pending', while
81
        for 'ubuntu' we care about the 'Published' packages.
82
        """
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
83
        if self._archive == 'debian':
84
            # Launchpad only tracks debian packages as "Pending", it doesn't mark
85
            # them Published
86
            return 'Pending'
87
        return 'Published'
88
89
    def _query_params(self):
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
90
        """Get the parameters defining our query.
91
        This defines the actions we are making against the archive.
92
        :return: A dict of query parameters.
93
        """
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
94
        params = {'ws.op': 'getPublishedSources',
95
                  'exact_match': 'true',
96
                  # If we need to use "" shouldn't we quote the project somehow?
97
                  'source_name': '"%s"' % (self._project,),
98
                  'status': self._publication_status(),
99
                  # We only need the latest one, the results seem to be properly
100
                  # most-recent-debian-version sorted
101
                  'ws.size': '1',
102
        }
103
        if self._series is not None:
104
            params['distro_series'] = '/%s/%s' % (self._archive, self._series)
105
        if self._pocket is not None:
106
            params['pocket'] = self._pocket
107
        return params
108
109
    def _query_URL(self):
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
110
        """Create the full URL that we need to query, including parameters."""
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
111
        params = self._query_params()
112
        # We sort to give deterministic results for testing
113
        encoded = urllib.urlencode(sorted(params.items()))
114
        return '%s?%s' % (self._archive_URL(), encoded)
115
116
    def _get_lp_info(self):
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
117
        """Place an actual HTTP query against the Launchpad service."""
118
        if json is None:
119
            return None
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
120
        query_URL = self._query_URL()
121
        try:
122
            req = urllib2.Request(query_URL)
123
            response = urllib2.urlopen(req)
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
124
            json_info = response.read()
6024.3.3 by John Arbash Meinel
Start at least testing the package_branch regex.
125
        # TODO: We haven't tested the HTTPError
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
126
        except (urllib2.URLError, urllib2.HTTPError), e:
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
127
            trace.mutter('failed to place query to %r' % (query_URL,))
128
            trace.log_exception_quietly()
129
            return None
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
130
        return json_info
131
132
    def _parse_json_info(self, json_info):
133
        """Parse the json response from Launchpad into objects."""
134
        if json is None:
135
            return None
5050.79.4 by John Arbash Meinel
Put several tests behind a Feature object.
136
        try:
137
            return json.loads(json_info)
138
        except Exception:
139
            trace.mutter('Failed to parse json info: %r' % (json_info,))
140
            trace.log_exception_quietly()
141
            return None
5050.79.3 by John Arbash Meinel
All the bits are now hooked up. The slow tests are disabled,
142
143
    def get_latest_version(self):
144
        """Get the latest published version for the given package."""
145
        json_info = self._get_lp_info()
146
        if json_info is None:
147
            return None
148
        info = self._parse_json_info(json_info)
5050.79.4 by John Arbash Meinel
Put several tests behind a Feature object.
149
        if info is None:
150
            return None
151
        try:
152
            entries = info['entries']
153
            if len(entries) == 0:
154
                return None
155
            return entries[0]['source_package_version']
156
        except KeyError:
157
            trace.log_exception_quietly()
158
            return None
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
159
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
160
    def place(self):
161
        """Text-form for what location this represents.
162
163
        Example::
164
            ubuntu, natty => Ubuntu Natty
165
            ubuntu, natty-proposed => Ubuntu Natty Proposed
166
        :return: A string representing the location we are checking.
167
        """
168
        place = self._archive
169
        if self._series is not None:
170
            place = '%s %s' % (place, self._series)
171
        if self._pocket is not None and self._pocket != 'Release':
172
            place = '%s %s' % (place, self._pocket)
173
        return place.title()
174
5050.79.2 by John Arbash Meinel
Start refactoring lp_api_lite and making it testable.
175
5050.79.1 by John Arbash Meinel
Bring Maxb's code for querying launchpads api via a REST request.
176
def get_latest_publication(archive, series, project):
177
    """Get the most recent publication for a given project.
178
179
    :param archive: Either 'ubuntu' or 'debian'
180
    :param series: Something like 'natty', 'sid', etc. Can be set as None. Can
181
        also include a pocket such as 'natty-proposed'.
182
    :param project: Something like 'bzr'
183
    :return: A version string indicating the most-recent version published in
184
        Launchpad. Might return None if there is an error.
185
    """
5050.79.4 by John Arbash Meinel
Put several tests behind a Feature object.
186
    lp = LatestPublication(archive, series, project)
187
    return lp.get_latest_version()
6024.3.5 by John Arbash Meinel
Pull out code into helper functions, which allows us to test it.
188
189
6024.3.7 by John Arbash Meinel
Add code to determine the moste recent tag.
190
def get_most_recent_tag(tag_dict, the_branch):
191
    """Get the most recent revision that has been tagged."""
192
    # Note: this assumes that a given rev won't get tagged multiple times. But
193
    #       it should be valid for the package importer branches that we care
194
    #       about
195
    reverse_dict = dict((rev, tag) for tag, rev in tag_dict.iteritems())
196
    the_branch.lock_read()
197
    try:
198
        last_rev = the_branch.last_revision()
199
        graph = the_branch.repository.get_graph()
200
        stop_revisions = (None, revision.NULL_REVISION)
201
        for rev_id in graph.iter_lefthand_ancestry(last_rev, stop_revisions):
202
            if rev_id in reverse_dict:
203
                return reverse_dict[rev_id]
204
    finally:
205
        the_branch.unlock()
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
206
207
6024.3.11 by John Arbash Meinel
More refactoring.
208
def _get_newest_versions(the_branch, latest_pub):
6024.3.10 by John Arbash Meinel
Try refactoring the code a bit per vila's suggestions.
209
    """Get information about how 'fresh' this packaging branch is.
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
210
6024.3.10 by John Arbash Meinel
Try refactoring the code a bit per vila's suggestions.
211
    :param the_branch: The Branch to check
212
    :param latest_pub: The LatestPublication used to check most recent
213
        published version.
6024.3.11 by John Arbash Meinel
More refactoring.
214
    :return: (latest_ver, branch_latest_ver)
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
215
    """
216
    t = time.time()
217
    latest_ver = latest_pub.get_latest_version()
218
    t_latest_ver = time.time() - t
219
    trace.mutter('LatestPublication.get_latest_version took: %.3fs'
220
                 % (t_latest_ver,))
6024.3.10 by John Arbash Meinel
Try refactoring the code a bit per vila's suggestions.
221
    if latest_ver is None:
6024.3.11 by John Arbash Meinel
More refactoring.
222
        return None, None
6024.3.10 by John Arbash Meinel
Try refactoring the code a bit per vila's suggestions.
223
    t = time.time()
224
    tags = the_branch.tags.get_tag_dict()
225
    t_tag_dict = time.time() - t
226
    trace.mutter('LatestPublication.get_tag_dict took: %.3fs' % (t_tag_dict,))
6024.3.11 by John Arbash Meinel
More refactoring.
227
    if latest_ver in tags:
228
        # branch might have a newer tag, but we don't really care
229
        return latest_ver, latest_ver
230
    else:
231
        best_tag = get_most_recent_tag(tags, the_branch)
232
        return latest_ver, best_tag
233
234
6024.3.12 by John Arbash Meinel
Give up on distinguishing trace.note from trace.warning.
235
def _report_freshness(latest_ver, branch_latest_ver, place, verbosity,
236
                      report_func):
6024.3.10 by John Arbash Meinel
Try refactoring the code a bit per vila's suggestions.
237
    """Report if the branch is up-to-date."""
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
238
    if latest_ver is None:
239
        if verbosity == 'all':
6024.3.12 by John Arbash Meinel
Give up on distinguishing trace.note from trace.warning.
240
            report_func('Most recent %s version: MISSING' % (place,))
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
241
        elif verbosity == 'short':
6024.3.12 by John Arbash Meinel
Give up on distinguishing trace.note from trace.warning.
242
            report_func('%s is MISSING a version' % (place,))
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
243
        return
6024.3.11 by John Arbash Meinel
More refactoring.
244
    elif latest_ver == branch_latest_ver:
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
245
        if verbosity == 'minimal':
246
            return
247
        elif verbosity == 'short':
6024.3.12 by John Arbash Meinel
Give up on distinguishing trace.note from trace.warning.
248
            report_func('%s is CURRENT in %s' % (latest_ver, place))
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
249
        else:
6024.3.12 by John Arbash Meinel
Give up on distinguishing trace.note from trace.warning.
250
            report_func('Most recent %s version: %s\n'
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
251
                       'Packaging branch status: CURRENT'
252
                       % (place, latest_ver))
253
    else:
254
        if verbosity in ('minimal', 'short'):
6024.3.11 by John Arbash Meinel
More refactoring.
255
            if branch_latest_ver is None:
256
                branch_latest_ver = 'Branch'
6024.3.12 by John Arbash Meinel
Give up on distinguishing trace.note from trace.warning.
257
            report_func('%s is OUT-OF-DATE, %s has %s'
258
                        % (branch_latest_ver, place, latest_ver))
6024.3.8 by John Arbash Meinel
Move 'place' logic onto LatestPublication.
259
        else:
6024.3.12 by John Arbash Meinel
Give up on distinguishing trace.note from trace.warning.
260
            report_func('Most recent %s version: %s\n'
261
                        'Packaging branch version: %s\n'
262
                        'Packaging branch status: OUT-OF-DATE'
263
                        % (place, latest_ver, branch_latest_ver))
6024.3.10 by John Arbash Meinel
Try refactoring the code a bit per vila's suggestions.
264
265
266
def report_freshness(the_branch, verbosity, latest_pub):
267
    """Report to the user how up-to-date the packaging branch is.
268
269
    :param the_branch: A Branch object
270
    :param verbosity: Can be one of:
271
        off: Do not print anything, and skip all checks.
272
        all: Print all information that we have in a verbose manner, this
273
             includes misses, etc.
274
        short: Print information, but only one-line summaries
275
        minimal: Only print a one-line summary when the package branch is
276
                 out-of-date
277
    :param latest_pub: A LatestPublication instance
278
    """
279
    if verbosity == 'off':
280
        return
281
    if verbosity is None:
282
        verbosity = 'all'
6024.3.11 by John Arbash Meinel
More refactoring.
283
    latest_ver, branch_ver = _get_newest_versions(the_branch, latest_pub)
6024.3.10 by John Arbash Meinel
Try refactoring the code a bit per vila's suggestions.
284
    place = latest_pub.place()
6024.3.12 by John Arbash Meinel
Give up on distinguishing trace.note from trace.warning.
285
    _report_freshness(latest_ver, branch_ver, place, verbosity,
286
                      trace.note)