flashbake-0.26.2/0000755000175000017500000000000011412701624013363 5ustar tgideontgideonflashbake-0.26.2/.gitignore0000644000175000017500000000013011235635220015346 0ustar tgideontgideon*.pyc .*.swp build dist MANIFEST flashbake.egg-info .project .pydevproject .ropeproject flashbake-0.26.2/PKG-INFO0000644000175000017500000000033711412701624014463 0ustar tgideontgideonMetadata-Version: 1.0 Name: flashbake Version: 0.26.2 Summary: UNKNOWN Home-page: http://thecommandline.net Author: Thomas Gideon Author-email: cmdln@thecommandline.net License: GPLv3 Description: UNKNOWN Platform: UNKNOWN flashbake-0.26.2/CREDITS.txt0000644000175000017500000000203411231666655015235 0ustar tgideontgideonFirst and foremost, I want to thank Cory Doctorow. His simple inquiries into using source control for his writing projects served as the original genesis of this project. I also want to thank all of the early adopters who helped improve the code and the documentation by simply asking questions and using the software. To date, I've also had a few contributors who have helped by submitting patches and working directly on the project: * Vaskin Kissoyan - docs, testing * Ben Snider, bensnider.com - the original code for microblogs.py, docs * garthrk, github.org/garthrk - random fixes and tests, docs * Jason Penney, jasonpenney.net - brain storming, random fixes and enhancements * Tony Giunta - alternate implementation of uptime that uses uptime command I want to give a special thanks to Jonathan Coulton, Brad Turcotte and Beatnik Turtle for freely offering their songs for download. Being able to grab a handful of tracks and import them into Banshee let me build that plugin very quickly, whislt listening to their awesome tunes to boot. flashbake-0.26.2/flashbake/0000755000175000017500000000000011412701624015303 5ustar tgideontgideonflashbake-0.26.2/flashbake/console.py0000755000175000017500000002203411412701230017314 0ustar tgideontgideon#!/usr/bin/env python ''' flashbake - wrapper script that will get installed by setup.py into the execution path ''' # copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . from flashbake import commit, context, control from flashbake.plugins import PluginError, PLUGIN_ERRORS from optparse import OptionParser from os.path import join, realpath import flashbake.git import fnmatch import logging import os.path import subprocess import sys VERSION = '0.26.2' pattern = '.flashbake' def main(): ''' Entry point used by the setup.py installation script. ''' # handle options and arguments parser = _build_parser() (options, args) = parser.parse_args() if options.quiet and options.verbose: parser.error('Cannot specify both verbose and quiet') # configure logging level = logging.INFO if options.verbose: level = logging.DEBUG if options.quiet: level = logging.ERROR logging.basicConfig(level=level, format='%(message)s') home_dir = os.path.expanduser('~') # look for plugin directory _load_plugin_dirs(options, home_dir) if len(args) < 1: parser.error('Must specify project directory.') sys.exit(1) project_dir = args[0] # look for user's default control file hot_files, control_config = _load_user_control(home_dir, project_dir, options) # look for project control file control_file = _find_control(parser, project_dir) if None == control_file: sys.exit(1) # emit the context message and exit if options.context_only: sys.exit(_context_only(options, project_dir, control_file, control_config, hot_files)) quiet_period = 0 if len(args) == 2: try: quiet_period = int(args[1]) except: parser.error('Quiet minutes, "%s", must be a valid number.' % args[1]) sys.exit(1) try: (hot_files, control_config) = control.parse_control(project_dir, control_file, control_config, hot_files) control_config.context_only = options.context_only control_config.dry_run = options.dryrun (hot_files, control_config) = control.prepare_control(hot_files, control_config) if options.purge: commit.purge(control_config, hot_files) else: commit.commit(control_config, hot_files, quiet_period) except (flashbake.git.VCError, flashbake.ConfigError), error: logging.error('Error: %s' % str(error)) sys.exit(1) except PluginError, error: _handle_bad_plugin(error) sys.exit(1) def multiple_projects(): usage = "usage: %prog [options] [quiet_min]" parser = OptionParser(usage=usage, version='%s %s' % ('%prog', VERSION)) parser.add_option('-o', '--options', dest='flashbake_options', default='', action='store', type='string', metavar='FLASHBAKE_OPTS', help="options to pass through to the 'flashbake' command. Use quotes to pass multiple arguments.") (options, args) = parser.parse_args() if len(args) < 1: parser.error('Must specify root search directory.') sys.exit(1) LAUNCH_DIR = os.path.abspath(sys.path[0]) flashbake_cmd = os.path.join(LAUNCH_DIR, "flashbake") flashbake_opts = options.flashbake_options.split() for project in _locate_projects(args[0]): print(project + ":") proc = [ flashbake_cmd ] + flashbake_opts + [project] if len(args) > 1: proc.append(args[1]) subprocess.call(proc) def _locate_projects(root): for path, dirs, files in os.walk(root): #@UnusedVariable for project_path in [os.path.normpath(path) for filename in files if fnmatch.fnmatch(filename, pattern)]: yield project_path def _build_parser(): usage = "usage: %prog [options] [quiet_min]" parser = OptionParser(usage=usage, version='%s %s' % ('%prog', VERSION)) parser.add_option('-c', '--context', dest='context_only', action='store_true', default=False, help='just generate and show the commit message, don\'t check for changes') parser.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False, help='include debug information in the output') parser.add_option('-q', '--quiet', dest='quiet', action='store_true', default=False, help='disable all output excepts errors') parser.add_option('-d', '--dryrun', dest='dryrun', action='store_true', default=False, help='execute a dry run') parser.add_option('-p', '--plugins', dest='plugin_dir', action='store', type='string', metavar='PLUGIN_DIR', help='specify an additional location for plugins') parser.add_option('-r', '--purge', dest='purge', action='store_true', default=False, help='purge any files that have been deleted from source control') return parser def _load_plugin_dirs(options, home_dir): plugin_dir = join(home_dir, '.flashbake', 'plugins') if os.path.exists(plugin_dir): real_plugin_dir = realpath(plugin_dir) logging.debug('3rd party plugin directory exists, adding: %s' % real_plugin_dir) sys.path.insert(0, real_plugin_dir) else: logging.debug('3rd party plugin directory doesn\'t exist, skipping.') logging.debug('Only stock plugins will be available.') if options.plugin_dir != None: if os.path.exists(options.plugin_dir): logging.debug('Adding plugin directory, %s.' % options.plugin_dir) sys.path.insert(0, realpath(options.plugin_dir)) else: logging.warn('Plugin directory, %s, doesn\'t exist.' % options.plugin_dir) def _load_user_control(home_dir, project_dir, options): control_file = join(home_dir, '.flashbake', 'config') if os.path.exists(control_file): (hot_files, control_config) = control.parse_control(project_dir, control_file) control_config.context_only = options.context_only else: hot_files = None control_config = None return hot_files, control_config def _find_control(parser, project_dir): control_file = join(project_dir, '.flashbake') # look for .control for backwards compatibility if not os.path.exists(control_file): control_file = join(project_dir, '.control') if not os.path.exists(control_file): parser.error('Could not find .flashbake or .control file in directory, "%s".' % project_dir) return None else: return control_file def _context_only(options, project_dir, control_file, control_config, hot_files): try: (hot_files, control_config) = control.parse_control(project_dir, control_file, control_config, hot_files) control_config.context_only = options.context_only (hot_files, control_config) = control.prepare_control(hot_files, control_config) msg_filename = context.buildmessagefile(control_config) message_file = open(msg_filename, 'r') try: for line in message_file: print line.strip() finally: message_file.close() os.remove(msg_filename) return 0 except (flashbake.git.VCError, flashbake.ConfigError), error: logging.error('Error: %s' % str(error)) return 1 except PluginError, error: _handle_bad_plugin(error) return 1 def _handle_bad_plugin(plugin_error): logging.debug('Plugin error, %s.' % plugin_error) if plugin_error.reason == PLUGIN_ERRORS.unknown_plugin or plugin_error.reason == PLUGIN_ERRORS.invalid_plugin: #@UndefinedVariable logging.error('Cannot load plugin, %s.' % plugin_error.plugin_spec) return if plugin_error.reason == PLUGIN_ERRORS.missing_attribute: #@UndefinedVariable logging.error('Plugin, %s, doesn\'t have the needed plugin attribute, %s.' \ % (plugin_error.plugin_spec, plugin_error.name)) return if plugin_error.reason == PLUGIN_ERRORS.invalid_attribute: #@UndefinedVariable logging.error('Plugin, %s, has an invalid plugin attribute, %s.' \ % (plugin_error.plugin_spec, plugin_error.name)) return if plugin_error.reason == PLUGIN_ERRORS.missing_property: logging.error('Plugin, %s, requires the config option, %s, but it was missing.' \ % (plugin_error.plugin_spec, plugin_error.name)) return flashbake-0.26.2/flashbake/control.py0000644000175000017500000000364111243602010017330 0ustar tgideontgideon''' Created on Aug 3, 2009 control.py - control file parsing and preparation. @author: cmdln ''' # copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . import flashbake import logging def parse_control(project_dir, control_file, config=None, results=None): """ Parse the dot-control file to get config options and hot files. """ logging.debug('Checking %s' % control_file) if None == results: hot_files = flashbake.HotFiles(project_dir) else: hot_files = results if None == config: control_config = flashbake.ControlConfig() else: control_config = config control_file = open(control_file, 'r') try: for line in control_file: # skip anything else if the config consumed the line if control_config.capture(line): continue hot_files.addfile(line.strip()) finally: control_file.close() return (hot_files, control_config) def prepare_control(hot_files, control_config): control_config.init() logging.debug("loading file plugins") for plugin in control_config.file_plugins: logging.debug("running plugin %s" % plugin) plugin.pre_process(hot_files, control_config) return (hot_files, control_config) flashbake-0.26.2/flashbake/__init__.py0000644000175000017500000004006711314237301017420 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' __init__.py - Shared classes and functions for the flashbake package.''' from flashbake.plugins import PluginError, PLUGIN_ERRORS from types import * import commands import flashbake.plugins #@UnresolvedImport import glob import logging import os import os.path import re import sys #@Reimport class ConfigError(Exception): pass class ControlConfig: """ Accumulates options from a control file for use by the core modules as well as for the plugins. Also handles boot strapping the configured plugins. """ def __init__(self): self.initialized = False self.dry_run = False self.extra_props = dict() self.prop_types = dict() self.plugin_names = list() self.msg_plugins = list() self.file_plugins = list() self.notify_plugins = list() self.git_path = None self.project_name = None def capture(self, line): ''' Parse a line from the control file if it is relevant to plugin configuration. ''' # grab comments but don't do anything if line.startswith('#'): return True # grab blanks but don't do anything if len(line.strip()) == 0: return True if line.find(':') > 0: prop_tokens = line.split(':', 1) prop_name = prop_tokens[0].strip() prop_value = prop_tokens[1].strip() if 'plugins' == prop_name: self.add_plugins(prop_value.split(',')) return True # hang onto any extra propeties in case plugins use them if not prop_name in self.__dict__: self.extra_props[prop_name] = prop_value; return True try: if prop_name in self.prop_types: prop_value = self.prop_types[prop_name](prop_value) self.__dict__[prop_name] = prop_value except: raise ConfigError( 'The value, %s, for option, %s, could not be parse as %s.' % (prop_value, prop_name, self.prop_types[prop_name])) return True return False def init(self): """ Do any property clean up, after parsing but before use """ if self.initialized == True: return self.initialized = True if len(self.plugin_names) == 0: raise ConfigError('No plugins configured!') self.share_property('git_path') self.share_property('project_name') all_plugins = list() with_deps = dict() for plugin_name in self.plugin_names: logging.debug("initalizing plugin: %s" % plugin_name) try: plugin = self.create_plugin(plugin_name) if len(plugin.dependencies()) == 0: all_plugins.append(plugin) else: dep = Dependency(plugin) dep.map(with_deps) if isinstance(plugin, flashbake.plugins.AbstractMessagePlugin): logging.debug("Message Plugin: %s" % plugin_name) # TODO add notion of dependency for ordering if 'flashbake.plugins.location:Location' == plugin_name: self.msg_plugins.insert(0, plugin) else: self.msg_plugins.append(plugin) if isinstance(plugin, flashbake.plugins.AbstractFilePlugin): logging.debug("File Plugin: %s" % plugin_name) self.file_plugins.append(plugin) if isinstance(plugin, flashbake.plugins.AbstractNotifyPlugin): logging.debug('Notify Plugin: %s' % plugin_name) self.notify_plugins.append(plugin) except PluginError, e: # re-raise critical plugin error if not e.reason == PLUGIN_ERRORS.ignorable_error: #@UndefinedVariable raise e # allow ignorable errors through with a warning logging.warning('Skipping plugin, %s, ignorable error: %s' % (plugin_name, e.name)) for plugin in all_plugins: plugin.share_properties(self) if plugin.plugin_spec in with_deps: for dep in with_deps[plugin.plugin_spec]: dep.satisfy(plugin, all_plugins) if len(Dependency.all) > 0: logging.error('Unsatisfied dependencies!') for plugin in all_plugins: plugin.capture_properties(self) plugin.init(self) def share_property(self, name, type=None): """ Declare a shared property, this way multiple plugins can share some value through the config object. """ if name in self.__dict__: return value = None if name in self.extra_props: value = self.extra_props[name] del self.extra_props[name] if type != None: try: value = type(value) except: raise ConfigError('Problem parsing %s for option %s' % (name, value)) self.__dict__[name] = value def add_plugins(self, plugin_names): # use a comprehension to ensure uniqueness [self.__add_last(inbound_name) for inbound_name in plugin_names] def create_plugin(self, plugin_spec): """ Initialize a plugin, including vetting that it meets the correct protocol; not private so it can be used in testing. """ if plugin_spec.find(':') < 0: logging.debug('Plugin spec not validly formed, %s.' % plugin_spec) raise PluginError(PLUGIN_ERRORS.invalid_plugin, plugin_spec) #@UndefinedVariable tokens = plugin_spec.split(':') module_name = tokens[0] plugin_name = tokens[1] try: __import__(module_name) except ImportError: logging.warn('Invalid module, %s' % plugin_name) raise PluginError(PLUGIN_ERRORS.unknown_plugin, plugin_spec) #@UndefinedVariable try: plugin_class = self.__forname(module_name, plugin_name) plugin = plugin_class(plugin_spec) except Exception, e: logging.debug(e) logging.debug('Couldn\'t load class %s' % plugin_spec) raise PluginError(PLUGIN_ERRORS.unknown_plugin, plugin_spec) #@UndefinedVariable is_message_plugin = isinstance(plugin, flashbake.plugins.AbstractMessagePlugin) is_file_plugin = isinstance(plugin, flashbake.plugins.AbstractFilePlugin) is_notify_plugin = isinstance(plugin, flashbake.plugins.AbstractNotifyPlugin) if not is_message_plugin and not is_file_plugin and not is_notify_plugin: raise PluginError(PLUGIN_ERRORS.invalid_type, plugin_spec) #@UndefinedVariable if is_message_plugin: self.__checkattr(plugin_spec, plugin, 'connectable', bool) self.__checkattr(plugin_spec, plugin, 'addcontext', MethodType) if is_file_plugin: self.__checkattr(plugin_spec, plugin, 'pre_process', MethodType) if is_notify_plugin: self.__checkattr(plugin_spec, plugin, 'warn', MethodType) return plugin def __add_last(self, plugin_name): if plugin_name in self.plugin_names: self.plugin_names.remove(plugin_name) self.plugin_names.append(plugin_name) def __checkattr(self, plugin_spec, plugin, name, expected_type): try: attrib = eval('plugin.%s' % name) except AttributeError: raise PluginError(PLUGIN_ERRORS.missing_attribute, plugin_spec, name) #@UndefinedVariable if not isinstance(attrib, expected_type): raise PluginError(PLUGIN_ERRORS.invalid_attribute, plugin_spec, name) #@UndefinedVariable # with thanks to Ben Snider # http://www.bensnider.com/2008/02/27/dynamically-import-and-instantiate-python-classes/ def __forname(self, module_name, plugin_name): ''' Returns a class of "plugin_name" from module "module_name". ''' __import__(module_name) module = sys.modules[module_name] classobj = getattr(module, plugin_name) return classobj class Dependency: all = list() def __init__(self, plugin): self.plugin self.dep_count = len(plugin.dependencies) def map(self, dep_map): for spec in self.plugin.dependencies(): if spec not in dep_map: dep_map[spec] = list() dep_map[spec].append(self) def satisfy(self, plugin, all_plugins): self.dep_count -= 1 if self.dep_count == 0: pos = all_plugins.index(plugin) all_plugins.insert(pos + 1) all.remove(self) class HotFiles: """ Track the files as they are parsed and manipulated with regards to their git status and the dot-control file. """ def __init__(self, project_dir): self.project_dir = os.path.realpath(project_dir) self.linked_files = dict() self.outside_files = set() self.control_files = set() self.not_exists = set() self.to_add = set() self.globs = dict() self.deleted = set() def addfile(self, filename): to_expand = os.path.join(self.project_dir, filename) file_exists = False logging.debug('%s: %s' % (filename, glob.glob(to_expand))) if sys.hexversion < 0x2050000: glob_iter = glob.glob(to_expand) else: glob_iter = glob.iglob(to_expand) pattern = re.compile('(\[.+\]|\*|\?)') if pattern.search(filename): glob_re = re.sub('\*', '.*', filename) glob_re = re.sub('\?', '.', glob_re) self.globs[filename] = glob_re for expanded_file in glob_iter: # track whether iglob iterates at all, if it does not, then the line # didn't expand to anything meaningful if not file_exists: file_exists = True # skip the file if some previous glob hit it if (expanded_file in self.outside_files or expanded_file in self.linked_files.keys()): continue # the commit code expects a relative path rel_file = self.__make_rel(expanded_file) # skip the file if some previous glob hit it if rel_file in self.control_files: continue # checking this after removing the expanded project directory # catches absolute paths to files outside the project directory if rel_file == expanded_file: self.outside_files.add(expanded_file) continue link = self.__check_link(expanded_file) if link == None: self.control_files.add(rel_file) else: self.linked_files[expanded_file] = link if not file_exists: self.putabsent(filename) def contains(self, filename): return filename in self.control_files def remove(self, filename): if filename in self.control_files: self.control_files.remove(filename) def putabsent(self, filename): self.not_exists.add(filename) def putneedsadd(self, filename): self.to_add.add(filename) def put_deleted(self, filename): def __in_target(file_spec): return file_spec in self.not_exists to_delete = self.from_glob(filename) logging.debug('To delete after matching %s' % to_delete) to_delete.append(filename) to_delete = filter(__in_target, to_delete) [self.not_exists.remove(file_spec) for file_spec in to_delete] self.deleted.add(filename) def from_glob(self, filename): """ Returns any original glob-based file specifications from the control file that would match the input filename. Useful for file plugins that add their own globs and need to correlate actual files that match their globs. """ def __match(file_tuple): return re.match(file_tuple[1], filename) != None matches = filter(__match, self.globs.iteritems()) matches = dict(matches) return matches.keys() def warnproblems(self): # print warnings for linked files for filename in self.linked_files.keys(): logging.info('%s is a link or its directory path contains a link.' % filename) # print warnings for files outside the project for filename in self.outside_files: logging.info('%s is outside the project directory.' % filename) # print warnings for files that do not exists for filename in self.not_exists: logging.info('%s does not exist.' % filename) # print warnings for files that were once under version control but have been deleted for filename in self.deleted: logging.info('%s has been deleted from version control.' % filename) def addorphans(self, git_obj, control_config): if len(self.to_add) == 0: return message_file = flashbake.context.buildmessagefile(control_config) to_commit = list() for orphan in self.to_add: logging.debug('Adding %s.' % orphan) add_output = git_obj.add(orphan) logging.debug('Add output, %s' % add_output) to_commit.append(orphan) logging.info('Adding new files, %s.' % to_commit) # consolidate the commit to be friendly to how git normally works if not control_config.dry_run: commit_output = git_obj.commit(message_file, to_commit) logging.debug('Commit output, %s' % commit_output) os.remove(message_file) def needs_warning(self): return (len(self.not_exists) > 0 or len(self.linked_files) > 0 or len(self.outside_files) > 0 or len(self.deleted) > 0) def __check_link(self, filename): # add, above, makes sure filename is always relative if os.path.islink(filename): return filename directory = os.path.dirname(filename) while (len(directory) > 0): # stop at the project directory, if it is in the path if directory == self.project_dir: break # stop at root, as a safety check though it should not happen if directory == os.sep: break if os.path.islink(directory): return directory directory = os.path.dirname(directory) return None def __make_rel(self, filepath): return self.__drop_prefix(self.project_dir, filepath) def __drop_prefix(self, prefix, filepath): if not filepath.startswith(prefix): return filepath if not prefix.endswith(os.sep): prefix += os.sep if sys.hexversion < 0x2060000: return filepath.replace(prefix, "") else: return os.path.relpath(filepath, prefix) def find_executable(executable): found = filter(lambda ex: os.path.exists(ex), map(lambda path_token: os.path.join(path_token, executable), os.getenv('PATH').split(os.pathsep))) if (len(found) == 0): return None return found[0] def executable_available(executable): return find_executable(executable) != None flashbake-0.26.2/flashbake/commit.py0000755000175000017500000002012411404443636017156 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' commit.py - Parses a project's control file and wraps git operations, calling the context script to build automatic commit messages as needed.''' import context import datetime import git import logging import os import re import sys DELETED_RE = re.compile('#\s*deleted:.*') def commit(control_config, hot_files, quiet_mins): # change to the project directory, necessary to find the .flashbake file and # to correctly refer to the project files by relative paths os.chdir(hot_files.project_dir) git_obj = git.Git(hot_files.project_dir, control_config.git_path) # the wrapper object ensures git is on the path # get the git status for the project git_status = git_obj.status() _handle_fatal(hot_files, git_status) # in particular find the existing entries that need a commit pending_re = re.compile('#\s*(renamed|copied|modified|new file):.*') now = datetime.datetime.today() quiet_period = datetime.timedelta(minutes=quiet_mins) to_commit = list() # first look in the files git already knows about logging.debug("Examining git status.") for line in git_status.splitlines(): if pending_re.match(line): pending_file = _trimgit(line) # not in the dot-control file, skip it if not (hot_files.contains(pending_file)): continue logging.debug('Parsing status line %s to determine commit action' % line) # remove files that will be considered for commit hot_files.remove(pending_file) # check the quiet period against mtime last_mod = os.path.getmtime(pending_file) pending_mod = datetime.datetime.fromtimestamp(last_mod) pending_mod += quiet_period # add the file to the list to include in the commit if pending_mod < now: to_commit.append(pending_file) logging.debug('Flagging file, %s, for commit.' % pending_file) else: logging.debug('Change for file, %s, is too recent.' % pending_file) _capture_deleted(hot_files, line) logging.debug('Examining unknown or unchanged files.') hot_files.warnproblems() # figure out what the status of the remaining files is for control_file in hot_files.control_files: # this shouldn't happen since HotFiles.addfile uses glob.iglob to expand # the original file lines which does so based on what is in project_dir if not os.path.exists(control_file): logging.debug('%s does not exist yet.' % control_file) hot_files.putabsent(control_file) continue status_output = git_obj.status(control_file) # needed for git >= 1.7.0.4 if status_output.find('Untracked files') > 0: hot_files.putneedsadd(control_file) continue if status_output.startswith('error'): # needed for git < 1.7.0.4 if status_output.find('did not match') > 0: hot_files.putneedsadd(control_file) logging.debug('%s exists but is unknown by git.' % control_file) else: logging.error('Unknown error occurred!') logging.error(status_output) continue # use a regex to match so we can enforce whole word rather than # substring matchs, otherwise 'foo.txt~' causes a false report of an # error control_re = re.compile('\<' + re.escape(control_file) + '\>') if control_re.search(status_output) == None: logging.debug('%s has no uncommitted changes.' % control_file) # if anything hits this block, we need to figure out why else: logging.error('%s is in the status message but failed other tests.' % control_file) logging.error('Try \'git status "%s"\' for more info.' % control_file) hot_files.addorphans(git_obj, control_config) for plugin in control_config.file_plugins: plugin.post_process(to_commit, hot_files, control_config) if len(to_commit) > 0: logging.info('Committing changes to known files, %s.' % to_commit) message_file = context.buildmessagefile(control_config) if not control_config.dry_run: # consolidate the commit to be friendly to how git normally works commit_output = git_obj.commit(message_file, to_commit) logging.debug(commit_output) os.remove(message_file) _send_commit_notice(control_config, hot_files, to_commit) logging.info('Commit for known files complete.') else: logging.info('No changes to known files found to commit.') if hot_files.needs_warning(): _send_warning(control_config, hot_files) else: logging.info('No missing or untracked files found, not sending warning notice.') def purge(control_config, hot_files): # change to the project directory, necessary to find the .flashbake file and # to correctly refer to the project files by relative paths os.chdir(hot_files.project_dir) git_obj = git.Git(hot_files.project_dir, control_config.git_path) # the wrapper object ensures git is on the path git_status = git_obj.status() _handle_fatal(hot_files, git_status) logging.debug("Examining git status.") for line in git_status.splitlines(): _capture_deleted(hot_files, line) if len(hot_files.deleted) > 0: logging.info('Committing removal of known files, %s.' % hot_files.deleted) message_file = context.buildmessagefile(control_config) if not control_config.dry_run: # consolidate the commit to be friendly to how git normally works commit_output = git_obj.commit(message_file, hot_files.deleted) logging.debug(commit_output) os.remove(message_file) logging.info('Commit for deleted files complete.') else: logging.info('No deleted files to purge') def _capture_deleted(hot_files, line): if DELETED_RE.match(line): deleted_file = _trimgit(line) # remove files that will are known to have been deleted hot_files.remove(deleted_file) hot_files.put_deleted(deleted_file) def _handle_fatal(hot_files, git_status): if git_status.startswith('fatal'): logging.error('Fatal error from git.') if 'fatal: Not a git repository' == git_status: logging.error('Make sure "git init" was run in %s' % os.path.realpath(hot_files.project_dir)) else: logging.error(git_status) sys.exit(1) def _trimgit(status_line): if status_line.find('->') >= 0: tokens = status_line.split('->') return tokens[1].strip() tokens = status_line.split(':') return tokens[1].strip() def _send_warning(control_config, hot_files): if (len(control_config.notify_plugins) == 0 and not control_config.dry_run): logging.info('Skipping notice, no notify plugins configured.') return for plugin in control_config.notify_plugins: plugin.warn(hot_files, control_config) def _send_commit_notice(control_config, hot_files, to_commit): if (len(control_config.notify_plugins) == 0 and not control_config.dry_run): logging.info('Skipping notice, no notify plugins configured.') return for plugin in control_config.notify_plugins: plugin.notify_commit(to_commit, hot_files, control_config) flashbake-0.26.2/flashbake/plugins/0000755000175000017500000000000011412701624016764 5ustar tgideontgideonflashbake-0.26.2/flashbake/plugins/growl.py0000644000175000017500000001223511314237301020470 0ustar tgideontgideon# copyright 2009 Jason Penney # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . # growl.py - Growl notification flashbake plugin from flashbake import plugins import flashbake import logging import os import re import subprocess class Growl(plugins.AbstractNotifyPlugin): def __init__(self, plugin_spec): plugins.AbstractPlugin.__init__(self, plugin_spec) self.define_property('host') self.define_property('port') self.define_property('password') self.define_property('growlnotify') self.define_property('title_prefix', default='fb:') def init(self, config): if self.growlnotify == None: self.growlnotify = flashbake.find_executable('growlnotify') if self.growlnotify == None: raise plugins.PluginError(plugins.PLUGIN_ERRORS.ignorable_error, #@UndefinedVariable self.plugin_spec, 'Could not find command, growlnotify.') # TODO: use netgrowl.py (or wait for GNTP support to be finalized # so it will support Growl for Windows as well) def growl_notify(self, title, message): args = [ self.growlnotify, '--name', 'flashbake' ] if self.host != None: args += [ '--udp', '--host', self.host] if self.port != None: args += [ '--port', self.port ] if self.password != None: args += [ '--password', self.password ] title = ' '.join([self.title_prefix, title]) args += ['--message', message, '--title', title] subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) def warn(self, hot_files, config): ''' Emits one message per file, with less explanation than the email plugin. The most common case is that one or two files will be off, a large number of them can be considered pathological, e.g. someone who didn't read the documentation about lack of support for symlinks, for instance. ''' # if calling growl locally, then the current user must be logged into the console if self.host == None and not self.__active_console(): logging.debug('Current user does not have console access.') return logging.debug('Trying to warn via growl.') project_name = os.path.basename(hot_files.project_dir) [self.growl_notify('Missing in project, %s' % project_name, 'The file, "%s", is missing.' % file) for file in hot_files.not_exists] [self.growl_notify('Deleted in project, %s' % project_name, 'The file, "%s", has been deleted from version control.' % file) for file in hot_files.deleted] [self.growl_notify('Link in project, %s' % project_name, 'The file, "%s", is a link.' % file) for (file, link) in hot_files.linked_files.iteritems() if file == link] [self.growl_notify('Link in project, %s' % project_name, 'The file, "%s", is a link to %s.' % (link, file)) for (file, link) in hot_files.linked_files.iteritems() if file != link] [self.growl_notify('External file in project, %s' % project_name, 'The file, "%s", exists outside of the project directory.' % file) for file in hot_files.outside_files] def notify_commit(self, to_commit, hot_files, config): logging.debug('Trying to notify via growl.') self.growl_notify(os.path.basename(hot_files.project_dir), 'Tracking changes to:\n' + '\n'.join(to_commit)) def __whoami(self): cmd = flashbake.find_executable('whoami') if cmd: proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) return proc.communicate()[0].strip() else: return None def __active_console(self): user = self.__whoami() if not user: return False cmd = flashbake.find_executable('who') if not cmd: return False proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) active = False for line in proc.communicate()[0].splitlines(): m = re.match('^%s\s+console.*$' % user, line) if m: active = True break return active flashbake-0.26.2/flashbake/plugins/mail.py0000644000175000017500000001040511314237301020255 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' Created on Jul 23, 2009 mail.py - plug-in to send notices via smtp. @author: cmdln ''' from flashbake import plugins import logging import os import smtplib import sys # Import the email modules we'll need if sys.hexversion < 0x2050000: from email.MIMEText import MIMEText #@UnusedImport else: from email.mime.text import MIMEText #@Reimport class Email(plugins.AbstractNotifyPlugin): def __init__(self, plugin_spec): plugins.AbstractPlugin.__init__(self, plugin_spec) self.define_property('notice_to', required=True) self.define_property('notice_from') self.define_property('smtp_host', default='localhost') self.define_property('smtp_port', int, default=25) def init(self, config): if self.notice_from == None: self.notice_from = self.notice_to def warn(self, hot_files, control_config): body = '' if len(hot_files.not_exists) > 0: body += '\nThe following files do not exist:\n\n' for file in hot_files.not_exists: body += '\t' + file + '\n' body += '\nMake sure there is not a typo in .flashbake and that you created/saved the file.\n' if len(hot_files.deleted) > 0: body += '\nThe following files have been deleted from version control:\n\n' for file in hot_files.deleted: body += '\t' + file + '\n' body += '\nYou may restore these files or remove them from .flashbake after running flashbake --purge ' body += 'in your project directory.\n' if len(hot_files.linked_files) > 0: body += '\nThe following files in .flashbake are links or have a link in their directory path.\n\n' for (file, link) in hot_files.linked_files.iteritems(): if file == link: body += '\t' + file + ' is a link\n' else: body += '\t' + link + ' is a link on the way to ' + file + '\n' body += '\nMake sure the physical file and its parent directories reside in the project directory.\n' if len(hot_files.outside_files) > 0: body += '\nThe following files in .flashbake are not in the project directory.\n\n' for file in hot_files.outside_files: body += '\t' + file + '\n' body += '\nOnly files in the project directory can be tracked and committed.\n' if control_config.dry_run: logging.debug(body) if self.notice_to != None: logging.info('Dry run, skipping email notice.') return # Create a text/plain message msg = MIMEText(body, 'plain') msg['Subject'] = ('Some files in %s do not exist' % os.path.realpath(hot_files.project_dir)) msg['From'] = self.notice_from msg['To'] = self.notice_to # Send the message via our own SMTP server, but don't include the # envelope header. logging.debug('\nConnecting to SMTP on host %s, port %d' % (self.smtp_host, self.smtp_port)) try: s = smtplib.SMTP() s.connect(host=self.smtp_host, port=self.smtp_port) logging.info('Sending notice to %s.' % self.notice_to) logging.debug(body) s.sendmail(self.notice_from, [self.notice_to], msg.as_string()) logging.info('Notice sent.') s.close() except Exception, e: logging.error('Couldn\'t connect, will send later.') logging.debug("SMTP Error:\n" + str(e)); flashbake-0.26.2/flashbake/plugins/weather.py0000644000175000017500000001257711314237301021006 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' weather.py - Stock plugin for adding weather information to context, must have TZ or /etc/localtime available to determine city from ISO ID. ''' from flashbake.plugins import AbstractMessagePlugin from flashbake.plugins.timezone import findtimezone from urllib2 import HTTPError, URLError import logging import re import timezone import urllib import urllib2 import xml.dom.minidom class Weather(AbstractMessagePlugin): def __init__(self, plugin_spec): AbstractMessagePlugin.__init__(self, plugin_spec, True) self.define_property('city') self.share_property('tz', plugin_spec=timezone.PLUGIN_SPEC) ## plugin uses location_location from Location plugin self.share_property('location_location') def addcontext(self, message_file, config): """ Add weather information to the commit message. Looks for weather_city: first in the config information but if that is not set, will try to use the system time zone to identify a city. """ if config.location_location == None and self.city == None: zone = findtimezone(config) if zone == None: city = None else: city = self.__parsecity(zone) else: if config.location_location == None: city = self.city else: city = config.location_location if None == city: message_file.write('Couldn\'t determine city to fetch weather.\n') return False # call the Google weather API with the city weather = self.__getweather(city) if len(weather) > 0: # there is also an entry for the key, wind_condition, in the weather # dictionary message_file.write('Current weather for %(city)s is %(condition)s (%(temp_f)sF/%(temp_c)sC) %(humidity)s\n'\ % weather) else: message_file.write('Couldn\'t fetch current weather for city, %s.\n' % city) return len(weather) > 0 def __getweather(self, city): """ This relies on Google's unpublished weather API which may change without notice. """ # unpublished API that iGoogle uses for its weather widget baseurl = 'http://www.google.com/ig/api?' # encode the sole paramtere for_city = baseurl + urllib.urlencode({'weather': city}) # necessary machinery to fetch a web page opener = urllib2.build_opener(urllib2.HTTPCookieProcessor()) try: logging.debug('Requesting page for %s.' % for_city) # open the weather API page request = opener.open(urllib2.Request(for_city, headers={'Accept-Charset': 'UTF-8'})) weather_xml = request.read() # figure out whether the response is other than utf-8 and decode if needed if 'Content-Type' in request.info(): content_type = request.info()['Content-Type'] charset_m = re.search('.*; charset=(.*)$', content_type) if charset_m is not None: req_charset = charset_m.group(1) logging.debug('Decoding using charset, %s, based on the response.' % req_charset) weather_xml = weather_xml.decode(req_charset) # the weather API returns some nice, parsable XML weather_dom = xml.dom.minidom.parseString(weather_xml.encode('utf8')) # just interested in the conditions at the moment current = weather_dom.getElementsByTagName("current_conditions") if current == None or len(current) == 0: return dict() weather = dict() weather['city'] = city for child in current[0].childNodes: if child.localName == 'icon': continue weather[child.localName] = child.getAttribute('data').strip() return weather except HTTPError, e: logging.error('Failed with HTTP status code %d' % e.code) return {} except URLError, e: logging.error('Plugin, %s, failed to connect with network.' % self.__class__) logging.debug('Network failure reason, %s.' % e.reason) return {} def __parsecity(self, zone): if None == zone: return None tokens = zone.split("/") if len(tokens) != 2: logging.warning('Zone id, "%s", doesn''t appear to contain a city.' % zone) # return non-zero so calling shell script can catch return None city = tokens[1] # ISO id's have underscores, convert to spaces for the Google API return city.replace("_", " ") flashbake-0.26.2/flashbake/plugins/music.py0000644000175000017500000000776211314244673020501 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . # # the iTunes class is based on the itunes.py by Andrew Wheiss, originally # licensed under an MIT License ''' music.py - Plugin for gathering last played tracks from music player. ''' from flashbake.plugins import AbstractMessagePlugin import flashbake import logging import os.path import sqlite3 import subprocess import time class Banshee(AbstractMessagePlugin): def __init__(self, plugin_spec): """ Add an optional property for specifying a different location for the Banshee database. """ AbstractMessagePlugin.__init__(self, plugin_spec) self.define_property('db', default=os.path.join(os.path.expanduser('~'), '.config', 'banshee-1', 'banshee.db')) self.define_property('limit', int, default=3) self.define_property('last_played_format') def addcontext(self, message_file, config): """ Open the Banshee database and query for the last played tracks. """ query = """\ select t.Title, a.Name, t.LastPlayedStamp from CoreTracks t join CoreArtists a on t.ArtistID = a.ArtistID order by LastPlayedStamp desc limit %d""" query = query.strip() % self.limit conn = sqlite3.connect(self.db) try: cursor = conn.cursor() logging.debug('Executing %s' % query) cursor.execute(query) results = cursor.fetchall() message_file.write('Last %d track(s) played in Banshee:\n' % len(results)) for result in results: last_played = time.localtime(result[2]) if self.last_played_format != None: logging.debug('Using format %s' % self.last_played_format) last_played = time.strftime(self.last_played_format, last_played) else: last_played = time.ctime(result[2]) message_file.write('"%s", by %s (%s)' % (result[0], result[1], last_played)) message_file.write('\n') except Exception, error: logging.error(error) conn.close() return True class iTunes(AbstractMessagePlugin): ''' Based on Andrew Heiss' plugin which is MIT licensed which should be compatible. ''' def __init__(self, plugin_spec): AbstractMessagePlugin.__init__(self, plugin_spec) self.define_property('osascript') def init(self, config): if self.osascript is None: self.osascript = flashbake.find_executable('osascript') def addcontext(self, message_file, config): """ Get the track info and write it to the commit message """ info = self.trackinfo() if info is None: message_file.write('Couldn\'t get current track.\n') else: message_file.write('Currently playing in iTunes:\n%s' % info) return True def trackinfo(self): ''' Call the AppleScript file. ''' if self.osascript is None: return None directory = os.path.dirname(__file__) script_path = os.path.join(directory, 'current_track.scpt') args = [self.osascript, script_path] proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) return proc.communicate()[0] flashbake-0.26.2/flashbake/plugins/timezone.py0000644000175000017500000000527611275072645021215 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' timezone.py - Stock plugin to find the system's time zone add to the commit message.''' from flashbake.plugins import AbstractMessagePlugin import logging import os PLUGIN_SPEC = 'flashbake.plugins.timezone:TimeZone' class TimeZone(AbstractMessagePlugin): def __init__(self, plugin_spec): AbstractMessagePlugin.__init__(self, plugin_spec, False) self.share_property('tz', plugin_spec=PLUGIN_SPEC) def addcontext(self, message_file, config): """ Add the system's time zone to the commit context. """ zone = findtimezone(config) if zone == None: message_file.write('Couldn\'t determine time zone.\n') else: message_file.write('Current time zone is %s\n' % zone) return True def findtimezone(config): # check the environment for the zone value zone = os.environ.get("TZ") logging.debug('Zone from env is %s.' % zone) # some desktops don't set the env var but /etc/timezone should # have the value regardless if None != zone: logging.debug('Returning env var value.') return zone # this is common on many *nix variatns logging.debug('Checking /etc/timezone') if os.path.exists('/etc/timezone'): zone_file = open('/etc/timezone') try: zone = zone_file.read() finally: zone_file.close() zone = zone.replace("\n", "") return zone # this is specific to OS X logging.debug('Checking /etc/localtime') if os.path.exists('/etc/localtime'): zone = os.path.realpath('/etc/localtime') (zone, city) = os.path.split(zone); (zone, continent) = os.path.split(zone); zone = os.path.join(continent, city) return zone logging.debug('Checking .flashbake') if 'timezone' in config.__dict__: zone = config.timezone return zone logging.warn('Could not get TZ from env var, /etc/timezone, or .flashbake.') zone = None return zone flashbake-0.26.2/flashbake/plugins/current_track.scpt0000644000175000017500000000340511275075532022540 0ustar tgideontgideonif checkProcess("iTunes") then tell application "iTunes" if player state is playing then set trck to current track set title_text to (get name of trck) set artist_text to (get artist of trck) set album_text to (get album of trck) set playpos to (get player position) set displayTime to (my calc_total_time(playpos)) set title_time to (get time of trck) set rate to (get rating of trck) / 20 set rate_text to "" repeat rate times set rate_text to rate_text & " * " end repeat set body_text to title_text & " " & artist_text & " - " & album_text & " " & displayTime & "/" & title_time & " - " & rate_text else set body_text to "Nothing playing in iTunes" end if end tell else set body_text to "iTunes is not open" end if ---------------------------------------------------------------- to calc_total_time(totalSeconds) set theHour to totalSeconds div 3600 if theHour is not 0 then copy (theHour as string) & ":" to theHour else set theHour to "" end if set theMinutes to (totalSeconds mod 3600) div 60 if theMinutes is not 0 then --if theMinutes is less than 10 then set theMinutes to "0" & (theMinutes as string) copy (theMinutes as string) & ":" to theMinutes else set theMinutes to "0:" end if set theSeconds to totalSeconds mod 60 if theSeconds is less than 10 then set theSeconds to "0" & (theSeconds as string) return theHour & theMinutes & theSeconds as string end calc_total_time ---------------------------------------------------------------- on checkProcess(processName) tell application "System Events" set isRunning to ((application processes whose (name is equal to processName)) count) end tell if isRunning is greater than 0 then return true else return false end if end checkProcessflashbake-0.26.2/flashbake/plugins/feed.py0000644000175000017500000000666711314237301020255 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' feed.py - Stock plugin that pulls latest n items from a feed by a given author. ''' import feedparser import logging from urllib2 import HTTPError, URLError from flashbake.plugins import AbstractMessagePlugin class Feed(AbstractMessagePlugin): def __init__(self, plugin_spec): AbstractMessagePlugin.__init__(self, plugin_spec, True) self.define_property('url', required=True) self.define_property('author') self.define_property('limit', int, False, 5) def addcontext(self, message_file, config): """ Add the matching items to the commit context. """ # last n items for m creator (title, last_items) = self.__fetchfeed() if len(last_items) > 0: if self.author == None: message_file.write('Last %(item_count)d entries from %(feed_title)s:\n'\ % {'item_count' : len(last_items), 'feed_title' : title}) else: message_file.write('Last %(item_count)d entries from %(feed_title)s by %(author)s:\n'\ % {'item_count' : len(last_items), 'feed_title' : title, 'author': self.author}) for item in last_items: # edit the '%s' if you want to add a label, like 'Title %s' to the output message_file.write('%s\n' % item['title']) message_file.write('%s\n' % item['link']) else: message_file.write('Couldn\'t fetch entries from feed, %s.\n' % self.url) return len(last_items) > 0 def __fetchfeed(self): """ Fetch up to the limit number of items from the specified feed with the specified creator. """ try: feed = feedparser.parse(self.url) if not 'title' in feed.feed: logging.info('Feed title is empty, feed is either malformed or unavailable.') return (None, {}) feed_title = feed.feed.title by_creator = [] for entry in feed.entries: if self.author != None and entry.author != self.author: continue title = entry.title title = title.encode('ascii', 'replace') link = entry.link by_creator.append({"title" : title, "link" : link}) if self.limit <= len(by_creator): break return (feed_title, by_creator) except HTTPError, e: logging.error('Failed with HTTP status code %d' % e.code) return (None, {}) except URLError, e: logging.error('Plugin, %s, failed to connect with network.' % self.__class__) logging.debug('Network failure reason, %s.' % e.reason) return (None, {}) flashbake-0.26.2/flashbake/plugins/__init__.py0000644000175000017500000001443311314237301021077 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . from enum import Enum import flashbake import logging PLUGIN_ERRORS = Enum('invalid_plugin', 'invalid_type', 'unknown_plugin', 'missing_attribute', 'invalid_attribute', 'missing_property', 'ignorable_error') class PluginError(Exception): def __init__(self, reason, plugin_spec, name=None): self.plugin_spec = plugin_spec self.reason = reason self.name = name def __str__(self): if self.name == None: return '%s: %s' % (self.reason, self.plugin_spec) else: return '%s, %s: %s' % (self.plugin_spec, self.reason, self.name) def service_and_prefix(plugin_spec): service_name = plugin_spec.split(':')[-1] property_prefix = '_'.join(service_name.lower().strip().split(' ')) return service_name, property_prefix class AbstractPlugin: """ Common parent for all kinds of plugins, mostly to share option handling code. """ def __init__(self, plugin_spec): self.plugin_spec = plugin_spec self.service_name, self.property_prefix = service_and_prefix(plugin_spec) self.__property_defs = [] self.__shared_prop_defs = [] def define_property(self, name, type=None, required=False, default=None): try: self.__property_defs.append((name, type, required, default)) except AttributeError: raise Exception('Call AbstractPlugin.__init__ in your plugin\'s __init__.') def share_property(self, name, type=None, plugin_spec=None): try: if plugin_spec: parsed = service_and_prefix(plugin_spec) property_prefix = parsed[1] self.__shared_prop_defs.append(('%s_%s' % (property_prefix, name), type)) else: self.__shared_prop_defs.append((name, type)) except AttributeError: raise Exception('Call AbstractPlugin.__init__ in your plugin\'s __init__.') def share_properties(self, config): for name, type in self.__shared_prop_defs: config.share_property(name, type) def capture_properties(self, config): try: for prop in self.__property_defs: assert len(prop) == 4, "Property definition, %s, is invalid" % (prop,) self.__capture_property(config, *prop) except AttributeError: raise Exception('Call AbstractPlugin.__init__ in your plugin\'s __init__.') def init(self, config): """ This method is optional. """ pass def dependencies(self): """ Optional method via which a plugin can express a dependency on another plugin. """ return list() def __capture_property(self, config, name, type=None, required=False, default=None): """ Move a property, if present, from the ControlConfig to the daughter plugin. """ config_name = '%s_%s' % (self.property_prefix, name) if required and not config_name in config.extra_props: raise PluginError(PLUGIN_ERRORS.missing_property, self.plugin_spec, config_name) value = default if config_name in config.extra_props: value = config.extra_props[config_name] del config.extra_props[config_name] if type != None and value != None: try: value = type(value) except: raise flashbake.ConfigError( 'The value, %s, for option, %s, could not be parsed as %s.' % (value, name, type)) self.__dict__[name] = value def abstract(self): """ borrowed this from Norvig http://norvig.com/python-iaq.html """ import inspect caller = inspect.getouterframes(inspect.currentframe())[1][3] raise NotImplementedError('%s must be implemented in subclass' % caller) class AbstractMessagePlugin(AbstractPlugin): """ Common parent class for all message plugins, will try to help enforce the plugin protocol at runtime. """ def __init__(self, plugin_spec, connectable=False): AbstractPlugin.__init__(self, plugin_spec) self.connectable = connectable def addcontext(self, message_file, config): """ This method is required, it will asplode if not overridden by daughter classes. """ self.abstract() class AbstractFilePlugin(AbstractPlugin): """ Common parent class for all file plugins, will try to help enforce the plugin protocol at runtime. """ def pre_process(self, hot_files, config): """ This method is required, it will asplode if not overridden by daughter classes. """ self.abstract() def post_process(self, to_commit, hot_files, config): """ This method is optional, it will be run after status processing but before commit so the plugin may shuffle files into the commit. """ pass class AbstractNotifyPlugin(AbstractPlugin): """ Common parent class for all notification plugins. """ def warn(self, hot_files, config): ''' Implementations will provide messages about the problem files in the hot_files argument through different mechanisms. N.B. This method is required, it will asplode if not overridden by daughter classes. ''' self.abstract() def notify_commit(self, to_commit, hot_files, config): ''' Option method to notify when a commit is performed, probably most useful for services like desktop notifiers. ''' pass flashbake-0.26.2/flashbake/plugins/microblog.py0000644000175000017500000001512311314237301021312 0ustar tgideontgideon# copyright 2009 Ben Snider (bensnider.com), Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' microblog.py - microblog plugins by Ben Snider, bensnider.com ''' from flashbake.plugins import AbstractMessagePlugin from urllib2 import HTTPError, URLError from xml.etree.ElementTree import ElementTree import logging import urllib class Twitter(AbstractMessagePlugin): def __init__(self, plugin_spec): AbstractMessagePlugin.__init__(self, plugin_spec, True) self.service_url = 'http://twitter.com' self.optional_field_info = { \ 'source':{'path':'source', 'transform':propercase}, \ 'location':{'path':'user/location', 'transform':propercase}, \ 'favorited':{'path':'favorited', 'transform':propercase}, \ 'tweeted_on': {'path':'created_at', 'transform':utc_to_local}, \ } self.define_property('user', required=True) self.define_property('limit', int, False, 5) self.define_property('optional_fields') def init(self, config): if self.limit > 200: logging.warn('Please use a limit <= 200.'); self.limit = 200 self.__setoptionalfields(config) # simple user xml feed self.twitter_url = '%(url)s/statuses/user_timeline/%(user)s.xml?count=%(limit)d' % { 'url':self.service_url, 'user':self.user, 'limit':self.limit} def __setoptionalfields(self, config): # We don't have to worry about a KeyError here since this property # should have been set to None by self.setoptionalproperty. if (self.optional_fields == None): self.optional_fields = [] else: # get the optional fields, split on commas fields = self.optional_fields.strip().split(',') newFields = [] for field in fields: field = field.strip() # check if they are allowed and not a dupe if (field in self.optional_field_info and field not in newFields): # if so we push them onto the optional fields array, otherwise ignore newFields.append(field) # finally sort the list so its the same each run, provided the config is the same newFields.sort() self.optional_fields = newFields def addcontext(self, message_file, config): (title, last_tweets) = self.__fetchitems(config) if (len(last_tweets) > 0 and title != None): to_file = ('Last %(item_count)d %(service_name)s messages from %(twitter_title)s:\n' \ % {'item_count' : len(last_tweets), 'twitter_title' : title, 'service_name':self.service_name}) i = 1 for item in last_tweets: to_file += ('%d) %s\n' % (i, item['tweet'])) for field in self.optional_fields: to_file += ('\t%s: %s\n' % (propercase(field), item[field])) i += 1 logging.debug(to_file.encode('UTF-8')) message_file.write(to_file.encode('UTF-8')) else: message_file.write('Couldn\'t fetch entries from feed, %s.\n' % self.twitter_url) return len(last_tweets) > 0 def __fetchitems(self, config): ''' We fetch the tweets from the configured url in self.twitter_url, and return a list containing the formatted title and an array of tweet dictionaries that contain at least the 'tweet' key along with any optional fields. The ''' results = [None, []] try: twitter_xml = urllib.urlopen(self.twitter_url) except HTTPError, e: logging.error('Failed with HTTP status code %d' % e.code) return results except URLError, e: logging.error('Plugin, %s, failed to connect with network.' % self.__class__) logging.debug('Network failure reason, %s.' % e.reason) return results except IOError: logging.error('Plugin, %s, failed to connect with network.' % self.__class__) logging.debug('Socket error.') return results tree = ElementTree() tree.parse(twitter_xml) status = tree.find('status') if (status == None): return results # after this point we are pretty much guaranteed that we won't get an # exception or None value, provided the twitter xml stays the same results[0] = propercase(status.find('user/name').text) for status in tree.findall('status'): tweet = {} tweet['tweet'] = status.find('text').text for field in self.optional_fields: tweet[field] = status.find(self.optional_field_info[field]['path']).text if ('transform' in self.optional_field_info[field]): tweet[field] = self.optional_field_info[field]['transform'](tweet[field]) results[1].append(tweet) return results class Identica(Twitter): def __init__(self, plugin_spec): Twitter.__init__(self, plugin_spec) self.service_url = 'http://identi.ca/api' self.optional_field_info['created_on'] = self.optional_field_info['tweeted_on'] del self.optional_field_info['tweeted_on'] def propercase(string): ''' Returns the string with _ replaced with spaces and the whole string should be title cased. ''' string = string.replace('_', ' ') string = string.title() return string def utc_to_local(t): ''' ganked from http://feihonghsu.blogspot.com/2008/02/converting-from-local-time-to-utc.html ''' import calendar, datetime # Discard the timezone, python dont like it, and it seems to always be # set to UTC, even if the user has their timezone set. t = t.replace('+0000 ', '') # might asplode return datetime.datetime.fromtimestamp((calendar.timegm(datetime.datetime.strptime(t, '%a %b %d %H:%M:%S %Y').timetuple()))).strftime("%A, %b. %d, %Y at %I:%M%p %z") flashbake-0.26.2/flashbake/plugins/scrivener.py0000644000175000017500000001536311314237301021343 0ustar tgideontgideon# copyright 2009 Jay Penney # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' scrivener.py - Scrivener flashbake plugin by Jason Penney, jasonpenney.net''' from flashbake.plugins import AbstractFilePlugin, AbstractMessagePlugin, PluginError, PLUGIN_ERRORS import flashbake #@UnusedImport import fnmatch import glob import logging import os import pickle import subprocess def find_scrivener_projects(hot_files, config, flush_cache=False): if flush_cache: config.scrivener_projects = None if config.scrivener_projects == None: scrivener_projects = list() for f in hot_files.control_files: if fnmatch.fnmatch(f, '*.scriv'): scrivener_projects.append(f) config.scrivener_projects = scrivener_projects return config.scrivener_projects def _relpath(path, start): path = os.path.realpath(path) start = os.path.realpath(start) if not path.startswith(start): raise Exception("unable to calculate paths") if os.path.samefile(path, start): return "." if not start.endswith(os.path.sep): start += os.path.sep return path[len(start):] def find_scrivener_project_contents(hot_files, scrivener_project): contents = list() for path, dirs, files in os.walk(os.path.join(hot_files.project_dir, scrivener_project)): #@UnusedVariable if hasattr(os.path, "relpath"): rpath = os.path.relpath(path, hot_files.project_dir) else: try: import pathutils #@UnresolvedImport rpath = pathutils.relative(path, hot_files.project_dir) except: rpath = _relpath(path, hot_files.project_dir) for filename in files: contents.append(os.path.join(rpath, filename)) return contents def get_logfile_name(scriv_proj_dir): return os.path.join(os.path.dirname(scriv_proj_dir), ".%s.flashbake.wordcount" % os.path.basename(scriv_proj_dir)) ## TODO: deal with deleted files class ScrivenerFile(AbstractFilePlugin): def __init__(self, plugin_spec): AbstractFilePlugin.__init__(self, plugin_spec) self.share_property('scrivener_projects') def pre_process(self, hot_files, config): for f in find_scrivener_projects(hot_files, config): logging.debug("ScrivenerFile: adding '%s'" % f) for hotfile in find_scrivener_project_contents(hot_files, f): #logging.debug(" - %s" % hotfile) hot_files.control_files.add(hotfile) def post_process(self, to_commit, hot_files, config): flashbake.commit.purge(config, hot_files) class ScrivenerWordcountFile(AbstractFilePlugin): """ Record Wordcount for Scrivener Files """ def __init__(self, plugin_spec): AbstractFilePlugin.__init__(self, plugin_spec) self.share_property('scrivener_projects') self.share_property('scrivener_project_count') def init(self, config): if not flashbake.executable_available('textutil'): raise PluginError(PLUGIN_ERRORS.ignorable_error, self.plugin_spec, 'Could not find command, textutil.') #@UndefinedVariable def pre_process(self, hot_files, config): config.scrivener_project_count = dict() for f in find_scrivener_projects(hot_files, config): scriv_proj_dir = os.path.join(hot_files.project_dir, f) hot_logfile = get_logfile_name(f) logfile = os.path.join(hot_files.project_dir, hot_logfile) if os.path.exists(logfile): logging.debug("logifile exists %s" % logfile) log = open(logfile, 'r') oldCount = pickle.load(log) log.close() else: oldCount = { 'Content': 0, 'Synopsis': 0, 'Notes' : 0, 'All' :0 } newCount = { 'Content': self.get_count(scriv_proj_dir, ["*[0-9].rtfd"]), 'Synopsis': self.get_count(scriv_proj_dir, ['*_synopsis.txt' ]), 'Notes': self.get_count(scriv_proj_dir, [ '*_notes.rtfd' ]), 'All': self.get_count(scriv_proj_dir, ['*.rtfd', '*.txt']) } config.scrivener_project_count[f] = { 'old': oldCount, 'new': newCount } if not config.context_only: log = open(logfile, 'w') pickle.dump(config.scrivener_project_count[f]['new'], log) log.close() if not hot_logfile in hot_files.control_files: hot_files.control_files.add(logfile) def get_count(self, file, matches): count = 0 args = ['textutil', '-stdout', '-cat', 'txt'] do_count = False for match in list(matches): for f in glob.glob(os.path.normpath(os.path.join(file, match))): do_count = True args.append(f) if do_count: p = subprocess.Popen(args, stdout=subprocess.PIPE, close_fds=True) for line in p.stdout: count += len(line.split(None)) return count class ScrivenerWordcountMessage(AbstractMessagePlugin): """ Display Wordcount for Scrivener Files """ def __init__(self, plugin_spec): AbstractMessagePlugin.__init__(self, plugin_spec, False) self.share_property('scrivener_project_count') def addcontext(self, message_file, config): to_file = '' if 'scrivener_project_count' in config.__dict__: for proj in config.scrivener_project_count: to_file += "Wordcount: %s\n" % proj for key in [ 'Content', 'Synopsis', 'Notes', 'All' ]: new = config.scrivener_project_count[proj]['new'][key] old = config.scrivener_project_count[proj]['old'][key] diff = new - old to_file += "- " + key.ljust(10, ' ') + str(new).rjust(20) if diff != 0: to_file += " (%+d)" % (new - old) to_file += "\n" message_file.write(to_file) flashbake-0.26.2/flashbake/plugins/location.py0000644000175000017500000001420411412701241021142 0ustar tgideontgideon# location.py # Net location plugin. # # copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . from flashbake.plugins import AbstractMessagePlugin from urllib2 import HTTPError, URLError from xml.dom import minidom import logging import os.path import re import urllib import urllib2 class Location(AbstractMessagePlugin): def __init__(self, plugin_spec): AbstractMessagePlugin.__init__(self, plugin_spec, True) self.share_property('location_location') def addcontext(self, message_file, config): ip_addr = self.__get_ip() if ip_addr == None: message_file.write('Failed to get public IP for geo location.\n') return False location = self.__locate_ip(ip_addr) if len(location) == 0: message_file.write('Failed to parse location data for IP address.\n') return False logging.debug(location) location_str = '%(City)s, %(RegionName)s' % location config.location_location = location_str message_file.write('Current location is %s based on IP %s.\n' % (location_str, ip_addr)) return True def __locate_ip(self, ip_addr): cached = self.__load_cache() if cached.get('ip_addr','') == ip_addr: del cached['ip_addr'] return cached base_url = 'http://ipinfodb.com/ip_query.php?' for_ip = base_url + urllib.urlencode({'ip': ip_addr}) opener = urllib2.build_opener(urllib2.HTTPCookieProcessor()) try: logging.debug('Requesting page for %s.' % for_ip) # open the location API page location_xml = opener.open(urllib2.Request(for_ip)).read() # the weather API returns some nice, parsable XML location_dom = minidom.parseString(location_xml) # just interested in the conditions at the moment response = location_dom.getElementsByTagName("Response") if response == None or len(response) == 0: return dict() location = dict() for child in response[0].childNodes: if child.localName == None: continue key = child.localName key = key.encode('ASCII', 'replace') location[key] = self.__get_text(child.childNodes) self.__save_cache(ip_addr, location) return location except HTTPError, e: logging.error('Failed with HTTP status code %d' % e.code) return {} except URLError, e: logging.error('Plugin, %s, failed to connect with network.' % self.__class__) logging.debug('Network failure reason, %s.' % e.reason) return {} def __load_cache(self): home_dir = os.path.expanduser('~') # look for flashbake directory fb_dir = os.path.join(home_dir, '.flashbake') cache = dict() if not os.path.exists(fb_dir): return cache cache_name = os.path.join(fb_dir, 'ip_cache') if not os.path.exists(cache_name): return cache cache_file = open(cache_name, 'r') try: for line in cache_file: tokens = line.split(':') key = tokens[0] value = tokens[1].strip() if key.startswith('location.'): key = key.replace('location.', '') cache[key] = value logging.debug('Loaded cache %s' % cache) finally: cache_file.close() return cache def __save_cache(self, ip_addr, location): home_dir = os.path.expanduser('~') # look for flashbake directory fb_dir = os.path.join(home_dir, '.flashbake') if not os.path.exists(fb_dir): os.mkdir(fb_dir) cache_file = open(os.path.join(fb_dir, 'ip_cache'), 'w') try: cache_file.write('ip_addr:%s\n' % ip_addr) for key in location.iterkeys(): cache_file.write('location.%s:%s\n' % (key, location[key])) finally: cache_file.close() def __get_text(self, node_list): text_value = '' for node in node_list: if node.nodeType != node.TEXT_NODE: continue; text_value += node.data return text_value def __get_ip(self): no_reply = 'http://www.noreply.org' opener = urllib2.build_opener(urllib2.HTTPCookieProcessor()) try: # open the weather API page ping_reply = opener.open(urllib2.Request(no_reply)).read() hello_line = None for line in ping_reply.split('\n'): if line.find('Hello') > 0: hello_line = line.strip() break if hello_line is None: logging.error('Failed to parse Hello with public IP address.') return None logging.debug(hello_line) m = re.search('([0-9]+\.){3}([0-9]+){1}', hello_line) if m is None: logging.error('Failed to parse Hello with public IP address.') return None ip_addr = m.group(0) return ip_addr except HTTPError, e: logging.error('Failed with HTTP status code %d' % e.code) return None except URLError, e: logging.error('Plugin, %s, failed to connect with network.' % self.__class__) logging.debug('Network failure reason, %s.' % e.reason) return None flashbake-0.26.2/flashbake/plugins/uptime.py0000644000175000017500000001052711314237301020643 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' uptime.py - Stock plugin to calculate the system's uptime and add to the commit message.''' from flashbake.plugins import AbstractMessagePlugin from subprocess import Popen, PIPE import flashbake import logging import os.path class UpTime(AbstractMessagePlugin): def addcontext(self, message_file, config): """ Add the system's up time to the commit context. """ uptime = self.__calcuptime() if uptime == None: message_file.write('Couldn\'t determine up time.\n') else: message_file.write('System has been up %s\n' % uptime) return True def __calcuptime(self): """ copied with blanket permission from http://thesmithfam.org/blog/2005/11/19/python-uptime-script/ """ if not os.path.exists('/proc/uptime'): return self.__run_uptime() f = open("/proc/uptime") try: contents = f.read().split() except: return None finally: f.close() total_seconds = float(contents[0]) # Helper vars: MINUTE = 60 HOUR = MINUTE * 60 DAY = HOUR * 24 # Get the days, hours, etc: days = int(total_seconds / DAY) hours = int((total_seconds % DAY) / HOUR) minutes = int((total_seconds % HOUR) / MINUTE) seconds = int(total_seconds % MINUTE) # Build up the pretty string (like this: "N days, N hours, N minutes, N seconds") string = "" if days > 0: string += str(days) + " " + (days == 1 and "day" or "days") + ", " if len(string) > 0 or hours > 0: string += str(hours) + " " + (hours == 1 and "hour" or "hours") + ", " if len(string) > 0 or minutes > 0: string += str(minutes) + " " + (minutes == 1 and "minute" or "minutes") + ", " string += str(seconds) + " " + (seconds == 1 and "second" or "seconds") return string def __run_uptime(self): """ For OSes that don't provide procfs, then try to use the updtime command. Thanks to Tony Giunta for this contribution. """ if not flashbake.executable_available('uptime'): return None # Try to capture output of 'uptime' command, # if not found, catch OSError, log and return None try: output = Popen("uptime", stdout=PIPE).communicate()[0].split() except OSError: logging.warn("Can't find 'uptime' command in $PATH") return None # Parse uptime output string # if len == 10 or 11, uptime is less than a day if len(output) in [10, 11]: days = "00" hours_and_minutes = output[2].strip(",") elif len(output) == 12: days = output[2] hours_and_minutes = output[4].strip(",") else: return None # If time is exactly x hours/mins, no ":" in "hours_and_minutes" # and the interpreter will throw a ValueError try: hours, minutes = hours_and_minutes.split(":") except ValueError: if output[3].startswith("hr"): hours = hours_and_minutes minutes = "00" elif output[3].startwwith("min"): hours = "00" minutes = hours_and_minutes else: return None # Build up output string, might require Python 2.5+ uptime = (days + (" day, " if days == "1" else " days, ") + hours + (" hour, " if hours == "1" else " hours, ") + minutes + (" minute" if minutes == "1" else " minutes")) return uptime flashbake-0.26.2/flashbake/git.py0000644000175000017500000000743311275072053016453 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' git.py - Wrap the call outs to git, adding sanity checks and environment set up if needed.''' import logging import os import subprocess class VCError(Exception): """ Error when the version control wrapper object cannot be set up. """ def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class Git(): def __init__(self, cwd, git_path=None): # look for git in the environment's PATH var path_env = os.getenv('PATH') if (len(path_env) == 0): path_env = os.defpath path_tokens = path_env.split(os.pathsep) git_exists = False # if there is a git_path option, that takes precedence if git_path != None: if git_path.endswith('git'): git_path = os.path.dirname(git_path) if os.path.exists(os.path.join(git_path, 'git')): git_exists = True else: for path_token in path_tokens: if os.path.exists(os.path.join(path_token, 'git')): git_exists = True # fail much sooner and more quickly then if git calls are made later, # naively assuming it is available if not git_exists: raise VCError('Could not find git executable on PATH.') # set up an environment mapping suitable for use with the subprocess # module self.__init_env(git_path) self.__cwd = cwd def status(self, filename=None): """ Get the git status for the specified files, or the entire current directory. """ if filename != None: files = list() files.append(filename) return self.__run('status', files=files) else: return self.__run('status') def add(self, file): """ Add an unknown but existing file. """ files = [ file ] return self.__run('add', files=files) def commit(self, messagefile, files): """ Commit a list of files, the files should be strings and quoted. """ options = ['-F', messagefile] return self.__run('commit', options, files) def __run(self, cmd, options=None, files=None): cmds = list() cmds.append('git') cmds.append(cmd) if options != None: cmds += options if files != None: cmds += files proc = subprocess.Popen(cmds, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=self.__cwd, env=self.env) return proc.communicate()[0] def __init_env(self, git_path): self.env = dict() self.env.update(os.environ) if git_path != None: new_path = self.env['PATH'] new_path = '%s%s%s' % (git_path, os.pathsep, new_path) self.env['PATH'] = new_path if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG, format='%(message)s') git = Git('../foo', '/opt/local/bin') try: git = Git('../foo') except VCError, e: logging.info(e) os.chdir('../foo') logging.info(git.status()) flashbake-0.26.2/flashbake/context.py0000755000175000017500000000363711314237301017352 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . ''' context.py - Build up some descriptive context for automatic commit to git''' import os.path import random def buildmessagefile(config): """ Build a commit message that uses the provided ControlConfig object and return a reference to the resulting file. """ config.init() msg_filename = '/tmp/git_msg_%d' % random.randint(0,1000) # try to avoid clobbering another process running this script while os.path.exists(msg_filename): msg_filename = '/tmp/git_msg_%d' % random.randint(0,1000) connectable = False connected = False message_file = open(msg_filename, 'w') try: for plugin in config.msg_plugins: plugin_success = plugin.addcontext(message_file, config) # let each plugin say which ones attempt network connections if plugin.connectable: connectable = True connected = connected or plugin_success if connectable and not connected: message_file.write('All of the plugins that use the network failed.\n') message_file.write('Your computer may not be connected to the network.') finally: message_file.close() return msg_filename flashbake-0.26.2/setup.py0000644000175000017500000000136211412701364015100 0ustar tgideontgideon#!/usr/bin/env python # # setup.py for flashbake from setuptools import setup, find_packages setup(name='flashbake', version='0.26.2', author="Thomas Gideon", author_email="cmdln@thecommandline.net", url="http://thecommandline.net", license="GPLv3", packages=find_packages(exclude=['test.*']), install_requires=''' enum >=0.4.3 feedparser >=4.1 ''', entry_points={ 'console_scripts': [ 'flashbake = flashbake.console:main', 'flashbakeall = flashbake.console:multiple_projects' ] }, include_package_data = True, exclude_package_data = { '' : [ 'test/*' ] } ) flashbake-0.26.2/test/0000755000175000017500000000000011412701624014342 5ustar tgideontgideonflashbake-0.26.2/test/files.py0000644000175000017500000000434111314237301016015 0ustar tgideontgideonimport commands import flashbake import os.path import unittest class FilesTestCase(unittest.TestCase): def setUp(self): test_dir = os.path.join(os.getcwd(), 'test') test_zip = os.path.join(test_dir, 'project.zip') commands.getoutput('unzip -d %s %s' % (test_dir, test_zip)) self.files = flashbake.HotFiles(os.path.join(test_dir, 'project')) self.project_files = [ 'todo.txt', 'stickies.txt', 'my stuff.txt', 'bar/novel.txt', 'baz/novel.txt', 'quux/novel.txt' ] def tearDown(self): commands.getoutput('rm -rf %s' % self.files.project_dir) def testrelative(self): for file in self.project_files: self.files.addfile(file) self.assertTrue(file in self.files.control_files, 'Should contain relative file, %s' % file) count = len(self.files.control_files) self.files.addfile('*add*') self.assertEquals(len(self.files.control_files), count + 3, 'Should have expanded glob.') def testabsolute(self): for file in self.project_files: abs_file = os.path.join(self.files.project_dir, file) self.files.addfile(abs_file) self.assertTrue(file in self.files.control_files, 'Should contain absolute file, %s, as relative path, %s.' % (abs_file, file)) count = len(self.files.control_files) self.files.addfile(os.path.join(self.files.project_dir, '*add*')) self.assertEquals(len(self.files.control_files), count + 3, 'Should have expanded glob.') def testabsent(self): self.files.addfile('does not exist.txt') self.files.addfile('doesn\'t exist.txt') self.files.addfile('does{not}exist.txt') self.assertEquals(len(self.files.not_exists), 3, 'None of the provided files should exist') def testoutside(self): self.files.addfile('/tmp') self.assertEquals(len(self.files.outside_files), 1, 'Outside files should get caught') def testlinks(self): self.files.addfile('link/novel.txt') self.assertEquals(len(self.files.linked_files.keys()), 1, 'Linked files should get caught') flashbake-0.26.2/test/project.zip0000644000175000017500000000713311156526307016550 0ustar tgideontgideonPK 'wm:project/UT ЬºI»¬ºIUxèèPK ÎXE: project/baz/UT „‹I»¬ºIUxèèPKÎXE: ø`«project/baz/novel.txtUT „‹I»¬ºIUxèèEÁnÃ0 Cïù õ>ôvë}ûÅf²TØn‚üýä E/D>’T4bÚQOZÄ@æ;ô>MßFxŠúúÂ>gPñÀ} üYÁeV÷ã”6®œ:j£ÄzõüJÈWQ¦¾ÁƉüÁ±g>j †B¡ù?p EoX¿!ï¢Êb—sxmW„’J‰MI4sïŠÛ³,K¬Xz×F\ÁÕi4öMl%V;>ïÓPKycl:-è$wproject/.flashbakeUT 8¹I»¬ºIUxèè}MOÃ0 †ïù“zàÖL .9!Ðà\e»DKâÒ8cÚ'ÍFW>¶KâÇ~ýÆqÑÚ¸0>ˆÆÊ çr å1S’q°E¬8¯ùI:v^ÛÞG¼·oé:Yi°Z»¹Ðëb0µ¸—>hvAØ(ñ˜ÖGUì¬ÐD­à. import sys from os.path import join, realpath, abspath import unittest import logging # just provide the command line hook into the tests if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG, format='%(message)s') LAUNCH_DIR = abspath(sys.path[0]) flashbake_dir = join(LAUNCH_DIR, "..") sys.path.insert(0, realpath(flashbake_dir)) try: from flashbake.commit import commit #@UnusedImport from flashbake.control import parse_control #@UnusedImport from flashbake.context import buildmessagefile #@UnusedImport import test.config import test.files finally: del sys.path[0] # combine classes into single suite config_suite = unittest.TestLoader().loadTestsFromTestCase(test.config.ConfigTestCase) files_suite = unittest.TestLoader().loadTestsFromTestCase(test.files.FilesTestCase) suite = unittest.TestSuite([config_suite, files_suite]) unittest.TextTestRunner(verbosity=2).run(suite) flashbake-0.26.2/test/plugins.py0000644000175000017500000000347311314237301016401 0ustar tgideontgideonfrom flashbake import ControlConfig import flashbake.plugins import logging import unittest class FilesTestCase(unittest.TestCase): def setUp(self): self.config = ControlConfig() def testrelative(self): pass class MissingParent(): def __init__(self, plugin_spec): pass def addcontext(self, message_file, control_config): logging.debug('do nothing') class NoConnectable(flashbake.plugins.AbstractMessagePlugin): def __init__(self, plugin_spec): pass def addcontext(self, message_file, control_config): logging.debug('do nothing') class NoAddContext(flashbake.plugins.AbstractMessagePlugin): def __init__(self, plugin_spec): flashbake.plugins.AbstractMessagePlugin.__init__(self, plugin_spec, True) class WrongConnectable(flashbake.plugins.AbstractMessagePlugin): def __init__(self, plugin_spec): self.connectable = 1 def addcontext(self, message_file, control_config): logging.debug('do nothing') class WrongAddContext(flashbake.plugins.AbstractMessagePlugin): def __init__(self, plugin_spec): self.connectable = True self.addcontext = 1 class Plugin1(flashbake.plugins.AbstractMessagePlugin): """ Sample plugin. """ def addcontext(self, message_file, config): """ Stub. """ pass class Plugin2(flashbake.plugins.AbstractMessagePlugin): """ Sample plugin. """ def dependencies(self): return ['test.plugins:Plugin1'] def addcontext(self, message_file, config): """ Stub. """ pass class Plugin3(flashbake.plugins.AbstractMessagePlugin): """ Sample plugin. """ def dependencies(self): return ['test.plugins:Plugin1', 'text.plugins:Plugin2'] def addcontext(self, message_file, config): """ Stub. """ pass flashbake-0.26.2/COPYING.txt0000644000175000017500000010762011231666655015257 0ustar tgideontgideon----- begin license block ----- GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ----- end license block ----- Universal Feed Parser (feedparser.py), its testing harness (feedparsertest.py), and its unit tests (everything in the tests/ directory) are released under the following license: ----- begin license block ----- Copyright (c) 2002-2005, Mark Pilgrim All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ----- end license block ----- flashbake-0.26.2/flashbake.egg-info/0000755000175000017500000000000011412701624016775 5ustar tgideontgideonflashbake-0.26.2/flashbake.egg-info/PKG-INFO0000644000175000017500000000033711412701621020072 0ustar tgideontgideonMetadata-Version: 1.0 Name: flashbake Version: 0.26.2 Summary: UNKNOWN Home-page: http://thecommandline.net Author: Thomas Gideon Author-email: cmdln@thecommandline.net License: GPLv3 Description: UNKNOWN Platform: UNKNOWN flashbake-0.26.2/flashbake.egg-info/top_level.txt0000644000175000017500000000001711412701621021522 0ustar tgideontgideontest flashbake flashbake-0.26.2/flashbake.egg-info/dependency_links.txt0000644000175000017500000000000111412701621023040 0ustar tgideontgideon flashbake-0.26.2/flashbake.egg-info/SOURCES.txt0000644000175000017500000000147711412701624020672 0ustar tgideontgideon.gitignore COPYING.txt CREDITS.txt setup.py flashbake/__init__.py flashbake/commit.py flashbake/console.py flashbake/context.py flashbake/control.py flashbake/git.py flashbake.egg-info/PKG-INFO flashbake.egg-info/SOURCES.txt flashbake.egg-info/dependency_links.txt flashbake.egg-info/entry_points.txt flashbake.egg-info/requires.txt flashbake.egg-info/top_level.txt flashbake/plugins/__init__.py flashbake/plugins/current_track.scpt flashbake/plugins/feed.py flashbake/plugins/growl.py flashbake/plugins/location.py flashbake/plugins/mail.py flashbake/plugins/microblog.py flashbake/plugins/music.py flashbake/plugins/scrivener.py flashbake/plugins/timezone.py flashbake/plugins/uptime.py flashbake/plugins/weather.py plugins/hellodolly.py test/__init__.py test/config.py test/files.py test/plugins.py test/project.zip test/test.pyflashbake-0.26.2/flashbake.egg-info/requires.txt0000644000175000017500000000003511412701621021370 0ustar tgideontgideonenum >=0.4.3 feedparser >=4.1flashbake-0.26.2/flashbake.egg-info/entry_points.txt0000644000175000017500000000015111412701621022265 0ustar tgideontgideon[console_scripts] flashbakeall = flashbake.console:multiple_projects flashbake = flashbake.console:main flashbake-0.26.2/setup.cfg0000644000175000017500000000007311412701624015204 0ustar tgideontgideon[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 flashbake-0.26.2/plugins/0000755000175000017500000000000011412701624015044 5ustar tgideontgideonflashbake-0.26.2/plugins/hellodolly.py0000644000175000017500000000171211412701270017563 0ustar tgideontgideon# copyright 2009 Thomas Gideon # # This file is part of flashbake. # # flashbake is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # flashbake is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with flashbake. If not, see . from flashbake.plugins import AbstractMessagePlugin class HelloDolly(AbstractMessagePlugin): """ Sample plugin. """ def addcontext(self, message_file, config): """ Stub. """ message_file.write('Hello, dolly.\n')