/brz/remove-bazaar

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