1
2
3
4 import os
5 import glob
6 import sys
7 import urllib
8 import tarfile
9
10 from moap.util import util, mail
11 from moap.doap import doap
12 import bug
13
14
16 opener = urllib.URLopener()
17 try:
18 (t, h) = opener.retrieve(url, filename)
19 except IOError, e:
20 if len(e.args) == 4:
21
22 if e.args[0] != 'http error':
23 raise e
24 code = e.args[1]
25 if code == 404:
26 print "URL %s not found" % url
27 raise e
28 else:
29 raise e
30
31
33 summary = "submit to Freshmeat"
34 description = """This command submits a release to Freshmeat.
35 Login details are taken from $HOME/.netrc. Add a section for a machine named
36 "freshmeat" with login and password settings.
37
38 Use --name if you want to override the project's name gotten from the .DOAP
39 file; to be used for example if your project uses dashes in the name which
40 Freshmeat does not allow.
41 """
42
44 self.parser.add_option('-b', '--branch',
45 action="store", dest="branch",
46 help="branch to submit, overriding the doap branch")
47 self.parser.add_option('-n', '--name',
48 action="store", dest="name",
49 help="name to submit, overriding the project name")
50
52 self.options = options
53
55 self.debug('submitting to freshmeat')
56 d = self.parentCommand.doap
57
58 if not self.parentCommand.version:
59 sys.stderr.write('Please specify a version to submit with -v.\n')
60 return 3
61
62
63 project = d.getProject()
64
65 from moap.publish import freshmeat
66 fm = freshmeat.Session()
67 try:
68 fm.login()
69 except freshmeat.SessionException, e:
70 sys.stderr.write('Could not login to Freshmeat: %s\n' %
71 e.message)
72 return 3
73
74
75 release = project.getRelease(self.parentCommand.version)
76 if not release:
77 sys.stderr.write('No revision %s found.\n' %
78 self.parentCommand.version)
79 return 3
80
81
82
83
84
85 branch = self.options.branch or release.version.branch or "Default"
86 name = self.options.name or project.shortname
87
88
89
90 args = {
91 'project_name': name,
92 'branch_name': branch,
93 'version': release.version.revision,
94 'changes': "Unknown",
95 'release_focus': 4,
96 'hide_from_frontpage': 'N',
97 }
98
99 for uri in release.version.file_release:
100 mapping = {
101 '.tar.gz': 'tgz',
102 '.tgz': 'tgz',
103 '.tar.bz2': 'bz2',
104 '.rpm': 'rpm',
105 }
106 for ext in mapping.keys():
107 if uri.endswith(ext):
108 key = 'url_%s' % mapping[ext]
109 self.stdout.write("- %s: %s\n" % (key, uri))
110 args[key] = uri
111
112 self.stdout.write(
113 "Submitting release of %s %s on branch %s\n" % (
114 project.name, self.parentCommand.version, branch))
115 try:
116 fm.publish_release(**args)
117 except freshmeat.SessionError, e:
118 if e.code == 40:
119 self.stderr.write("ERROR: denied releasing %r\n" %
120 self.parentCommand.version)
121 if e.code == 30:
122 self.stderr.write(
123 """ERROR: Freshmeat does not know the branch '%s'.
124 Most projects on Freshmeat have a branch named Default.
125 You can override the branch name manually with -b/--branch.
126 """ % branch)
127 elif e.code == 51:
128 self.stderr.write(
129 "Freshmeat already knows about this version\n")
130 elif e.code == 81:
131 self.stderr.write("Freshmeat does not know the project %s\n" %
132 project.shortname)
133 else:
134 self.stderr.write("ERROR: %r\n" % e)
135
136 -class Search(util.LogCommand):
137 description = "look up rank of project's home page based on keywords"
138
139 _engines = ["google", "yahoo"]
140 _default = "yahoo"
141
143 self.parser.add_option('-e', '--engine',
144 action="store", dest="engine", default=self._default,
145 help="search engine to use (out of %s; defaults to %s)" % (
146 ", ".join(self._engines), self._default))
147 self.parser.add_option('-l', '--limit',
148 action="store", dest="limit", default="100",
149 help="maximum number of results to look at")
150
152 self._limit = int(options.limit)
153 self._engine = options.engine
154
155 - def do(self, args):
156 if not args:
157 self.stderr.write('Please provide a search query.\n')
158 return 3
159
160 d = self.parentCommand.doap
161 project = d.getProject()
162
163 rank = 0
164 found = False
165 query = " ".join(args)
166
167 def foundURL(target, url):
168
169 if target.endswith('/'):
170 target = target[:-1]
171 if url.endswith('/'):
172 url = url[:-1]
173 return url == target
174
175 if self._engine == 'google':
176 from pygoogle import google
177
178 while not found:
179 self.debug('Doing Google search for %s starting from %d' % (
180 " ".join(args), rank))
181 value = google.doGoogleSearch(" ".join(args), start=rank)
182 for result in value.results:
183 rank += 1
184 self.debug('Hit %d: URL %s' % (rank, result.URL))
185 if foundURL(project.homepage, result.URL):
186 found = True
187 break
188
189 if rank >= self._limit:
190 break
191
192 elif self._engine == 'yahoo':
193 from yahoo.search import web
194
195
196 while not found:
197 search = web.WebSearch('moapmoap', query=query, start=rank+1)
198 info = search.parse_results()
199 for result in info.results:
200 rank += 1
201 self.debug('Hit %d: URL %s' % (rank, result['Url']))
202 if foundURL(project.homepage, result['Url']):
203 found = True
204 break
205
206 if rank >= self._limit:
207 break
208
209 else:
210 self.stderr.write("Unknown search engine '%s'.\n" % self._engine)
211 self.stderr.write("Please choose from %s.\n" %
212 ", ".join(self._engines))
213 return 3
214
215 if found:
216 self.stdout.write("Found homepage as hit %d\n." % rank)
217 else:
218 self.stdout.write("Did not find homepage in first %d hits.\n" %
219 self._limit)
220
221 -class Ical(util.LogCommand):
222 description = "Output iCal stream from project releases"
223
224 - def do(self, args):
225 __pychecker__ = 'no-argsused'
226 self.stdout.write("""BEGIN:VCALENDAR
227 PRODID:-//thomas.apestaart.org//moap//EN
228 VERSION:2.0
229
230 """)
231 entries = []
232 i = 0
233 for d in self.parentCommand.doaps:
234 i += 1
235 project = d.getProject()
236
237 for r in project.release:
238 d = {
239 'projectName': project.name,
240 'projectId': project.shortname,
241 'revision': r.version.revision,
242 'name': r.version.name,
243 'created': r.version.created,
244
245 'date': "".join(r.version.created.split('-')),
246 }
247 entries.append((r.version.created, i, d))
248
249
250 entries.sort()
251 for c, i, d in entries:
252
253 self.stdout.write("""BEGIN:VEVENT
254 SUMMARY:%(projectName)s %(revision)s '%(name)s' released
255 UID:%(created)s-%(projectId)s-%(revision)s@moap
256 CLASS:PUBLIC
257 PRIORITY:3
258 DTSTART;VALUE=DATE:%(date)s
259 DTEND;VALUE=DATE:%(date)s
260 END:VEVENT
261
262 """ % d)
263
264 self.stdout.write("\nEND:VCALENDAR\n")
265
266 -class Mail(util.LogCommand):
267 summary = "send release announcement through mail"
268 usage = "[mail-options] [TO]..."
269 description = """Send out release announcement mail.
270 The To: addresses can be specified as arguments to the mail command."""
271
273 self.parser.add_option('-f', '--from',
274 action="store", dest="fromm",
275 help="address to send from")
276 self.parser.add_option('-n', '--dry-run',
277 action="store_true", dest="dry_run",
278 help="show the mail that would have been sent")
279 self.parser.add_option('-R', '--release-notes',
280 action="store", dest="release_notes",
281 help="release notes to use (otherwise looked up in tarball)")
282
284 self.options = options
285
286 - def do(self, args):
287 d = self.parentCommand.doap
288
289 if not self.parentCommand.version:
290 sys.stderr.write('Please specify a version to submit with -v.\n')
291 return 3
292
293 version = self.parentCommand.version
294
295 if not self.options.fromm:
296 sys.stderr.write('Please specify a From: address with -f.\n')
297 return 3
298
299 if len(args) < 1:
300 sys.stderr.write('Please specify one or more To: addresses.\n')
301 return 3
302 to = args
303
304 project = d.getProject()
305
306 release = project.getRelease(version)
307 if not release:
308 sys.stderr.write('No revision %s found.\n' % version)
309 return 3
310
311
312 keep = []
313 extensions = ['.tar.gz', '.tgz', '.tar.bz2']
314 for uri in release.version.file_release:
315 for ext in extensions:
316 if uri.endswith(ext):
317 keep.append(uri)
318
319 self.debug('Release files: %r' % keep)
320
321
322
323 found = False
324 for uri in keep:
325 filename = os.path.basename(uri)
326 if os.path.exists(filename):
327 sys.stdout.write("Found release %s in current directory.\n" %
328 filename)
329 found = True
330 break
331
332
333 if not found:
334 for uri in keep:
335 if uri.startswith('http') or uri.startswith('ftp:'):
336 filename = os.path.basename(uri)
337 sys.stdout.write('Downloading %s ... ' % uri)
338 sys.stdout.flush()
339 urlgrab(uri, filename)
340 sys.stdout.write('done.\n')
341
342 sys.stdout.write(
343 "Downloaded %s in current dir\n" % filename)
344 found = True
345 break
346
347 if not found:
348 self.stderr.write("ERROR: no file found\n")
349 return 1
350
351
352 self.debug('Found %s' % filename)
353
354
355 RELEASE = None
356 if self.options.release_notes:
357 RELEASE = open(self.options.release_notes).read()
358 else:
359 tar_archive = tarfile.open(mode="r", name=filename)
360 for tarinfo in tar_archive:
361 if tarinfo.name.endswith('RELEASE'):
362 RELEASE = tar_archive.extractfile(tarinfo).read()
363 tar_archive.close()
364
365
366 d = {
367 'projectName': project.name,
368 'version': version,
369 'releaseName': release.version.name
370 }
371 subject = "RELEASE: %(projectName)s %(version)s '%(releaseName)s'" % d
372 content = "This mail announces the release of "
373 content += "%(projectName)s %(version)s '%(releaseName)s'.\n\n" % d
374 content += "%s\n" % project.description
375 if project.homepage:
376 content += "For more information, see %s\n" % project.homepage
377 if project.bug_database:
378 content += "To file bugs, go to %s\n" % project.bug_database
379
380 message = mail.Message(subject, to, self.options.fromm)
381 message.setContent(content)
382
383 if RELEASE:
384 message.addAttachment('RELEASE', 'text/plain', RELEASE)
385
386 if self.options.dry_run:
387 self.stdout.write(message.get())
388 else:
389 self.stdout.write('Sending release announcement ... ')
390 message.send()
391 self.stdout.write('sent.\n')
392
393 return 0
394
396 description = "Output RSS 2 feed from project releases"
397
399 self.parser.add_option('-t', '--template-language',
400 action="store", dest="language",
401 help="template language to use (genshi/cheetah)")
402
404 self._language = options.language or 'genshi'
405
407 from moap.doap import rss
408 template = None
409
410
411 if args:
412
413
414 path = args[0]
415 try:
416 handle = open(path)
417 template = handle.read()
418 handle.close()
419 except:
420 self.stderr.write("Could not read template %s.\n" % path)
421 return 3
422 self.debug("Using requested template %s" % template)
423
424
425 text = rss.doapsToRss(self.parentCommand.doaps, template,
426 templateType=self._language)
427 self.stdout.write(text)
428
429 -class Show(util.LogCommand):
430 description = "Show project information"
431
432 - def do(self, args):
458
459 -class Doap(util.LogCommand):
460 """
461 @ivar doap: the L{doap.Doap} object.
462 """
463
464 usage = "[doap-options] %command"
465 description = "read and act on DOAP file"
466 subCommandClasses = [Freshmeat, Ical, Mail, Rss, Search, Show, bug.Bug]
467
468 doap = None
469
471 self.parser.add_option('-f', '--file',
472 action="append", dest="files",
473 help=".doap file(s) to act on (glob wildcards allowed)")
474 self.parser.add_option('-v', '--version',
475 action="store", dest="version",
476 help="version to submit")
477
479 self.paths = []
480 self.doaps = []
481 if options.files:
482 for f in options.files:
483 self.paths.extend(glob.glob(f))
484 self.debug('%d doap paths' % len(self.paths))
485 self.version = options.version
486
487 if not self.paths:
488
489 try:
490 self.doap = doap.findDoapFile(None)
491 self.doaps = [self.doap, ]
492 except doap.DoapException, e:
493 sys.stdout.write(e.args[0])
494 return 3
495
496 return
497
498 for p in self.paths:
499 try:
500 d = doap.findDoapFile(p)
501 except doap.DoapException, e:
502 sys.stdout.write(e.args[0])
503 return 3
504 self.doaps.append(d)
505
506 self.doap = self.doaps[0]
507