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:
|