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