~arsenal-devel/arsenal/python-launchpadlib-toolkit

31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
1
#!/usr/bin/python
2
44 by Kamran Riaz Khan
Updated Attachment classes to migrate Launchpad related functions
3
import gzip
4
import os
5
import shutil
6
import tarfile
7
8
from copy                       import copy
9
from locale                     import getpreferredencoding
10
from zipfile                    import ZipFile
11
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
12
from attachment                 import Attachment
38.1.15 by Bryce Harrington
Add tests for recently added functionality, and fix things up to pass
13
from fnmatch                    import fnmatch
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
14
15
# Attachments
16
#
17
# A collection class for files added into launchpad, also known
18
# as an attachment.
19
#
44 by Kamran Riaz Khan
Updated Attachment classes to migrate Launchpad related functions
20
21
class LocalAttachment(object):
22
    class Data:
23
        class Fd(file):
24
            @property
25
            def content_type(self):
26
                return None
27
28
            @property
29
            def len(self):
30
                stat = os.stat(self.name)
31
                return stat.st_size
32
33
        def set_path(self, path):
34
            self.__path = path
35
        def open(self):
36
            if self.__path:
37
                return LocalAttachment.Data.Fd(self.__path)
38
39
    def __init__(self):
40
        self.data = LocalAttachment.Data()
41
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
42
class Attachments(object):
43
    # __init__
44
    #
37 by Bryce Harrington
Typo
45
    # Initialize the instance from a Launchpad bug.
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
46
    #
47
    def __init__(self, tkbug):
38.1.1 by Bryce Harrington
Allow filtering the attachments to exclude ones that don't match certain
48
        self.__tkbug                = tkbug
49
        self.__commit_changes       = tkbug.commit_changes
50
        self.__attachments          = None
51
        self.__filters              = []
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
52
44 by Kamran Riaz Khan
Updated Attachment classes to migrate Launchpad related functions
53
        self.__download             = False
54
        self.__download_dir         = None
55
56
        self.__extract              = False
57
        self.__extract_dir          = None
58
        self.__extract_limit        = None
59
60
        self.__gzip                 = False
61
        self.__gzip_dir             = None
62
63
        self.__force_mimetype       = False
64
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
65
    # __len__
66
    #
67
    def __len__(self):
38.1.15 by Bryce Harrington
Add tests for recently added functionality, and fix things up to pass
68
        return len(list(self.__iter__()))
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
69
70
    # __getitem__
71
    #
72
    def __getitem__(self, key):
38.1.15 by Bryce Harrington
Add tests for recently added functionality, and fix things up to pass
73
        return list(self.__iter__())[key]
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
74
75
    # __iter__
76
    #
77
    def __iter__(self):
38.1.4 by Bryce Harrington
Use of filtered_attachments shortcircuted yield; instead inline the
78
        self.__fetch_if_needed()
79
        for attachment in self.__attachments:
44 by Kamran Riaz Khan
Updated Attachment classes to migrate Launchpad related functions
80
            if self.__gzip:
81
                attachment = self.__gzip_if_needed(attachment)
82
38.1.15 by Bryce Harrington
Add tests for recently added functionality, and fix things up to pass
83
            included = True
44 by Kamran Riaz Khan
Updated Attachment classes to migrate Launchpad related functions
84
            a = Attachment(self.__tkbug, attachment, self.__force_mimetype)
38.1.7 by Bryce Harrington
tuples are iterated directly, not like dicts
85
            for f, params in self.__filters:
38.1.4 by Bryce Harrington
Use of filtered_attachments shortcircuted yield; instead inline the
86
                if not f(a, params):
38.1.15 by Bryce Harrington
Add tests for recently added functionality, and fix things up to pass
87
                    included = False
88
            if included:
44 by Kamran Riaz Khan
Updated Attachment classes to migrate Launchpad related functions
89
                if self.__extract and \
90
                            a.is_archive_type('tar') and \
91
                            not self.__exceeds_tar_limit(a.remotefd):
92
                    for member in self.__get_tar_members(a):
93
                        yield member
94
                elif self.__extract and \
95
                            a.is_archive_type('zip') and \
96
                            not self.__exceeds_zip_limit(a.remotefd):
97
                    for member in self.__get_zip_members(a):
98
                        yield member
99
                else:
100
                    if self.__download and \
101
                            not isinstance(attachment, LocalAttachment):
102
                        tmpfile = os.path.join(self.__download_dir, a.title)
103
                        with open(tmpfile, 'w+b') as localfd:
104
                            localfd.write(a.content)
105
                    yield a
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
106
107
    # __contains__
108
    #
109
    def __contains__(self, item):
38.1.15 by Bryce Harrington
Add tests for recently added functionality, and fix things up to pass
110
        return item in self.__iter__()
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
111
112
    # __fetch_if_needed
113
    #
114
    def __fetch_if_needed(self):
115
        if self.__attachments == None:
116
            self.__attachments = self.__tkbug.lpbug.attachments_collection
117
44 by Kamran Riaz Khan
Updated Attachment classes to migrate Launchpad related functions
118
    def __gzip_if_needed(self, attachment):
119
        filters = dict(self.__filters)
120
        if not filters.has_key(filter_size_between):
121
            return attachment
122
123
        minsize, maxsize = filters[filter_size_between]
124
        remotefd = attachment.data.open()
125
126
        if remotefd.len > maxsize:
127
            gzip_attachment = LocalAttachment()
128
            gzip_attachment.title = attachment.title + '.gz'
129
            gzip_attachment.type = attachment.type
130
131
            tmpfile = os.path.join(self.__gzip_dir, gzip_attachment.title)
132
            gzipfd = gzip.open(tmpfile, 'w+b')
133
            gzipfd.write(remotefd.read())
134
            gzipfd.close()
135
136
            gzip_attachment.data.set_path(tmpfile)
137
            attachment = gzip_attachment
138
139
        remotefd.close()
140
        return attachment
141
142
    def __find_mime_type(self, attachment):
143
        for pattern, mimetype in self.MIMETYPES.iteritems():
144
            if fnmatch(attachment.title, pattern):
145
                return mimetype
146
147
    def __get_tar_members(self, tarattachment):
148
        tar = tarfile.open(fileobj=tarattachment.remotefd)
149
150
        attachments = []
151
        for member in tar:
152
            if member.isfile():
153
                attachment = LocalAttachment()
154
                attachment.title = os.path.basename(member.name)
155
                if (fnmatch(attachment.title, '*.diff') or
156
                    fnmatch(attachment.title, '*.patch')):
157
                    attachment.type = 'Patch'
158
                else:
159
                    attachment.type = None
160
161
                tar.extract(member, self.__extract_dir)
162
                oldpath = os.path.join(self.__extract_dir,
163
                                       member.name)
164
                newpath = os.path.join(self.__extract_dir,
165
                                       os.path.basename(member.name))
166
                shutil.move(oldpath, newpath)
167
                attachment.data.set_path(newpath)
168
169
                attachments.append(Attachment(self.__tkbug,
170
                                              attachment,
171
                                              self.__force_mimetype))
172
173
        return attachments
174
175
    def __exceeds_tar_limit(self, fd):
176
        if not self.__extract_limit:
177
            return False
178
179
        tar = tarfile.open(fileobj=fd)
180
        result = len(tar.getnames()) > self.__extract_limit
181
        fd.seek(0)
182
183
        return result
184
185
    def __get_zip_members(self, zipattachment):
186
        zip = ZipFile(file=zipattachment.remotefd)
187
188
        attachments = []
189
        for member in [zip.open(name) for name in zip.namelist()]:
190
            if member.name[-1] == '/':
191
                continue
192
193
            attachment = LocalAttachment()
194
            attachment.title = os.path.basename(member.name)
195
            if (fnmatch(attachment.title, '*.diff') or
196
                fnmatch(attachment.title, '*.patch')):
197
                attachment.type = 'Patch'
198
            else:
199
                attachment.type = None
200
201
            zip.extract(member.name, self.__extract_dir)
202
            oldpath = os.path.join(self.__extract_dir, member.name)
203
            newpath = os.path.join(self.__extract_dir,
204
                                   os.path.basename(member.name))
205
            shutil.move(oldpath, newpath)
206
            attachment.data.set_path(newpath)
207
208
            attachments.append(Attachment(self.__tkbug,
209
                                          attachment,
210
                                          self.__force_mimetype))
211
212
        return attachments
213
214
    def __exceeds_zip_limit(self, fd):
215
        if not self.__extract_limit:
216
            return False
217
218
        zip = ZipFile(file=fd)
219
        result = len(zip.namelist()) > self.__extract_limit
220
        fd.seek(0)
221
222
        return result
223
224
    def download_in_dir(self, download_dir):
225
        self.__download = True
226
        self.__download_dir = download_dir
227
228
    def extract_archives(self, extract_dir, extract_limit=None):
229
        self.__extract = True
230
        self.__extract_dir = extract_dir
231
        self.__extract_limit = extract_limit
232
233
    def try_gzip(self, gzip_dir):
234
        self.__gzip = True
235
        self.__gzip_dir = gzip_dir
236
237
    def force_mimetype(self):
238
        self.__force_mimetype = True
239
38.1.1 by Bryce Harrington
Allow filtering the attachments to exclude ones that don't match certain
240
    def add_filter(self, f, params):
241
        """ Add filter f to constrain the list of attachments.
242
243
        f is a function which takes as arguments an Attachment
244
        object, and a list of parameters specific to the given
245
        filter.
246
        """
38.1.15 by Bryce Harrington
Add tests for recently added functionality, and fix things up to pass
247
        self.__filters.append( (f, params) )
38.1.1 by Bryce Harrington
Allow filtering the attachments to exclude ones that don't match certain
248
38.1.5 by Bryce Harrington
Add routine for determining if an Attachments collection includes all
249
    def check_required_files(self, glob_patterns):
250
        """ Check that collection includes required filenames
251
252
        Given a list of glob filename patterns, looks through the
253
        attachments to verify at least one attachment fulfils the
254
        required file pattern.  Returns a list of globs that were
255
        not matched.  Returns an empty list if all requirements
256
        were met.
257
        """
258
        missing = []
259
        for glob_pattern in glob_patterns:
260
            found = False
261
38.1.15 by Bryce Harrington
Add tests for recently added functionality, and fix things up to pass
262
            for a in self.__iter__():
263
                if fnmatch(a.filename, glob_pattern):
38.1.5 by Bryce Harrington
Add routine for determining if an Attachments collection includes all
264
                    found = True
265
                    break
266
267
            if not found:
268
                missing.append(glob_pattern)
269
        return missing
270
38.1.1 by Bryce Harrington
Allow filtering the attachments to exclude ones that don't match certain
271
# Filters
272
def filter_owned_by_person(attachment, persons):
273
    """ File owned by specific person(s) (e.g. original bug reporter) """
274
    return bool(attachment.owner and
275
                attachment.owner in persons)
276
277
def filter_filename_matches_globs(attachment, glob_patterns):
278
    """ Filename matches one of a set of glob patterns (e.g. Xorg.*.log) """
279
    filename = attachment.title
280
    for glob_pattern in glob_patterns:
38.1.6 by Bryce Harrington
Typo
281
        if fnmatch(filename, glob_pattern):
44 by Kamran Riaz Khan
Updated Attachment classes to migrate Launchpad related functions
282
            return False
283
    return True
38.1.1 by Bryce Harrington
Allow filtering the attachments to exclude ones that don't match certain
284
285
def filter_size_between(attachment, sizes):
286
    """ File size is within [min, max] bounds """
287
    assert(len(sizes) == 2)
288
    min_size = sizes[0]
289
    max_size = sizes[1]
290
    return bool(min_size <= len(attachment) and len(attachment) <= max_size)
291
292
def filter_age_between(attachment, ages_in_days):
293
    """ File was attached to bug between [min, max] days """
294
    assert(len(ages_in_days) == 2)
295
    min_age = ages_in_days[0]
296
    max_age = ages_in_days[1]
297
    return bool(min_age <= attachment.age and attachment.age <= max_age)
298
45 by Kamran Riaz Khan
Added filter_is_patch
299
def filter_is_patch(attachment, is_patch):
300
    return attachment.is_patch() == is_patch
301
31.1.4 by Brad Figg
- Added Attachments class (attachments.py) which encapsulates the Launchpad
302
# vi:set ts=4 sw=4 expandtab: