source: trunk/moap/command/doap.py @ 228

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