diff --git a/dosage b/dosage index 523139174..046b6fbad 100755 --- a/dosage +++ b/dosage @@ -28,6 +28,10 @@ from dosagelib.util import is_tty, get_columns, internal_error from dosagelib.configuration import App, Freeware, Copyright def setupOptions(): + """Construct option parser. + @return: new option parser + @rtype optparse.OptionParser + """ usage = 'usage: %prog [options] comicModule [comicModule ...]' parser = optparse.OptionParser(usage=usage) parser.add_option('-v', '--verbose', action='count', dest='verbose', default=0, help='provides verbose output, use multiple times for more verbosity') @@ -44,30 +48,44 @@ def setupOptions(): parser.add_option('-p', '--progress', action='store_true', dest='progress', default=False, help='display progress bar while downloading comics') return parser + +def displayVersion(): + """Display application name, version, copyright and license.""" + print App + print Copyright + print Freeware + + class Dosage(object): + """Main program executing comic commands.""" def __init__(self, settings): + """Store settings and initialize internal variables.""" self.settings = settings self.errors = 0 def setOutputInfo(self): + """Set global output level and timestamp option.""" out.level = 0 out.level += self.settings['verbose'] out.timestamps = self.settings['timestamps'] def saveComic(self, comic): + """Save one comic strip in an output file.""" basepath = self.settings['basepath'] progress = self.settings.get('progress', False) fn, saved = comic.save(basepath, progress) return saved def saveComics(self, comics): + """Save a list of comics.""" saved = False for comic in comics: saved = self.saveComic(comic) or saved return saved def safeOp(self, fp, *args, **kwargs): + """Run a function and catch and report any errors.""" try: fp(*args, **kwargs) except Exception: @@ -79,10 +97,12 @@ class Dosage(object): out.writelines(traceback.format_exception_only(type, value)) def getCurrent(self): + """Retrieve and save all current comic strips.""" out.write('Retrieving the current strip...') self.saveComics(self.module.getCurrentComics()) def getIndex(self, index): + """Retrieve comcis with given index.""" out.write('Retrieving index "%s"....' % (index,)) try: self.module.setStrip(index) @@ -91,12 +111,14 @@ class Dosage(object): out.write('No indexed retrieval support.') def catchup(self): + """Save all comics until the current date.""" out.write('Catching up...') for comics in self.module: if not self.saveComics(comics) and self.settings['catchup'] < 2: break def catchupIndex(self, index): + """Retrieve and save all comics from the given index.""" out.write('Catching up from index "%s"...' % (index,)) self.module.setStrip(index) for comics in self.module: @@ -104,15 +126,18 @@ class Dosage(object): break def getScrapers(self): + """Get list of scraper objects.""" return scraper.items() def getExistingComics(self): + """Get all existing comic scrapers.""" for scraper in self.getScrapers(): dirname = scraper.get_name().replace('/', os.sep) if os.path.isdir(os.path.join(self.settings['basepath'], dirname)): yield scraper def doList(self, columnList): + """List available comics.""" out.write('Available comic scrapers:') scrapers = self.getScrapers() if len(scrapers) > 0: @@ -123,9 +148,11 @@ class Dosage(object): out.write('%d supported comics.' % len(scrapers)) def doSingleList(self, scrapers): + """Get list of scraper names, one per line.""" print '\n'.join(scraper.get_name() for scraper in scrapers) def doColumnList(self, scrapers): + """Get list of scraper names with multiple names per line.""" screenWidth = get_columns() names = [scraper.get_name() for scraper in scrapers] maxlen = max([len(name) for name in names]) @@ -135,6 +162,7 @@ class Dosage(object): del names[:namesPerLine] def doCatchup(self): + """Catchup comics.""" for comic in self.useComics(): if self.indices: self.safeOp(self.catchupIndex, self.indices[0]) @@ -142,6 +170,7 @@ class Dosage(object): self.safeOp(self.catchup) def doCurrent(self): + """Get current comics.""" for comic in self.useComics(): if self.indices: for index in self.indices: @@ -150,16 +179,19 @@ class Dosage(object): self.safeOp(self.getCurrent) def doHelp(self): + """Print help for comic strips.""" for scraper in self.useComics(): for line in scraper.getHelp().splitlines(): out.write("Help: "+line) def setupComic(self, scraper): + """Setup the internal comic module from given scraper.""" self.module = scraper() out.context = scraper.get_name() return self.module def useComics(self): + """Set all comic modules for the defined comics.""" for comic in self.comics: c = comic.split(':', 2) if len(c) > 1: @@ -177,12 +209,8 @@ class Dosage(object): else: yield self.setupComic(scraper.get(moduleName)) - def displayVersion(self): - print App - print Copyright - print Freeware - def run(self, comics): + """Execute comic commands.""" self.setOutputInfo() self.comics = comics @@ -191,7 +219,7 @@ class Dosage(object): events.handler.start() if self.settings['version']: - self.displayVersion() + displayVersion() elif self.settings['list']: self.doList(self.settings['list'] == 1) elif len(comics) <= 0: @@ -206,6 +234,7 @@ class Dosage(object): events.handler.end() def main(): + """Parse options and execute commands.""" try: parser = setupOptions() options, args = parser.parse_args() diff --git a/dosagelib/comic.py b/dosagelib/comic.py index 522d246d3..d513cf23d 100644 --- a/dosagelib/comic.py +++ b/dosagelib/comic.py @@ -14,12 +14,26 @@ from .util import urlopen, saneDataSize, normaliseURL from .progress import progressBar, OperationComplete from .events import handler -class FetchComicError(IOError): pass +class FetchComicError(IOError): + """Exception for comic fetching errors.""" + pass class Comic(object): + """Download and save a single comic.""" + def __init__(self, moduleName, url, referrer=None, filename=None): + """Set URL and filename.""" self.moduleName = moduleName - url = normaliseURL(url) + self.url = normaliseURL(url) + self.referrer = referrer + if filename is None: + filename = url.split('/')[-1] + self.filename, self.ext = os.path.splitext(filename) + self.filename = self.filename.replace(os.sep, '_') + self.ext = self.ext.replace(os.sep, '_') + + def connect(self): + """Connect to host and get meta information.""" out.write('Getting headers for %s...' % (url,), 2) try: self.urlobj = urlopen(url, referrer=referrer) @@ -30,9 +44,6 @@ class Comic(object): self.urlobj.info().gettype() not in ('application/octet-stream', 'application/x-shockwave-flash'): raise FetchComicError, ('No suitable image found to retrieve.', url) - self.filename, self.ext = os.path.splitext(url.split('/')[-1]) - self.filename = filename or self.filename - self.filename = self.filename.replace(os.sep, '_') # Always use mime type for file extension if it is sane. if self.urlobj.info().getmaintype() == 'image': self.ext = '.' + self.urlobj.info().getsubtype() @@ -41,6 +52,7 @@ class Comic(object): out.write('... filename = "%s", ext = "%s", contentLength = %d' % (self.filename, self.ext, self.contentLength), 2) def touch(self, filename): + """Set last modified date on filename.""" if self.lastModified: tt = rfc822.parsedate(self.lastModified) if tt: @@ -48,6 +60,8 @@ class Comic(object): os.utime(filename, (mtime, mtime)) def save(self, basepath, showProgress=False): + """Save comic URL to filename on disk.""" + self.connect() comicName, comicExt = self.filename, self.ext comicSize = self.contentLength comicDir = os.path.join(basepath, self.moduleName.replace('/', os.sep)) diff --git a/dosagelib/configuration.py b/dosagelib/configuration.py index 505f5fad6..8e2033666 100644 --- a/dosagelib/configuration.py +++ b/dosagelib/configuration.py @@ -1,3 +1,6 @@ +""" +Define basic configuration data like version or application name. +""" import _Dosage_configdata as configdata Version = configdata.version diff --git a/dosagelib/events.py b/dosagelib/events.py index c2ac82e9d..c6848f07f 100644 --- a/dosagelib/events.py +++ b/dosagelib/events.py @@ -7,7 +7,11 @@ import urllib import util class EventHandler(object): + """Base class for writing events to files. The currently defined events are + start(), comicDownloaded() and end().""" + def __init__(self, basepath, baseurl): + """Initialize base path and url.""" self.basepath = basepath self.baseurl = baseurl or self.getBaseUrl() @@ -21,35 +25,46 @@ class EventHandler(object): return 'file:///' + url + '/' def getUrlFromFilename(self, filename): + """Construct URL from filename.""" components = util.splitpath(util.getRelativePath(self.basepath, filename)) url = '/'.join([urllib.quote(component, '') for component in components]) return self.baseurl + url def start(self): + """Emit a start event. Should be overridden in subclass.""" pass def comicDownloaded(self, comic, filename): + """Emit a comic downloaded event. Should be overridden in subclass.""" pass def end(self): + """Emit an end event. Should be overridden in subclass.""" pass class TextEventHandler(EventHandler): + """Output nothing. XXX why?""" pass class RSSEventHandler(EventHandler): + """Output in RSS format.""" + def RFC822Date(self, indate): + """Format date in rfc822 format. XXX move to util module.""" return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(indate)) def getFilename(self): + """Return RSS filename.""" return os.path.abspath(os.path.join(self.basepath, 'dailydose.rss')) def start(self): + """Log start event.""" today = time.time() yesterday = today - 86400 today = time.localtime(today) yesterday = time.localtime(yesterday) + # XXX replace with conf var link = 'https://github.com/wummel/dosage' self.rssfn = self.getFilename() @@ -62,6 +77,7 @@ class RSSEventHandler(EventHandler): self.rss = rss.Feed('Daily Dosage', link, 'Comics for %s' % time.strftime('%Y/%m/%d', today)) def comicDownloaded(self, comic, filename): + """Write RSS entry for downloaded comic.""" url = self.getUrlFromFilename(filename) args = ( '%s - %s' % (comic, os.path.basename(filename)), @@ -77,16 +93,22 @@ class RSSEventHandler(EventHandler): self.rss.insertHead(*args) def end(self): + """Write RSS data to file.""" self.rss.write(self.rssfn) + class HtmlEventHandler(EventHandler): + """Output in HTML format.""" + def fnFromDate(self, date): + """Get filename from date.""" fn = time.strftime('comics-%Y%m%d.html', date) fn = os.path.join(self.basepath, 'html', fn) fn = os.path.abspath(fn) return fn def start(self): + """Start HTML output.""" today = time.time() yesterday = today - 86400 tomorrow = today + 86400 @@ -117,12 +139,14 @@ class HtmlEventHandler(EventHandler): self.lastComic = None def comicDownloaded(self, comic, filename): + """Write HTML entry for downloaded comic.""" if self.lastComic != comic: self.newComic(comic) url = self.getUrlFromFilename(filename) self.html.write('