Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

merged with upstream

Merge branch 'master' of git://github.com/commandline/flashbake

Conflicts:
	flashbake/plugins/location.py
	flashbake/plugins/timezone.py
  • Loading branch information...
commit 7d99eb4d2dc63b5cb1cd2d377ac8668390cb79ae 2 parents 3808bfa + af56ada
@jpenney jpenney authored
View
3  .gitignore
@@ -4,3 +4,6 @@ build
dist
MANIFEST
flashbake.egg-info
+.project
+.pydevproject
+.ropeproject
View
22 LICENSE.txt → COPYING.txt
@@ -1,25 +1,3 @@
-flashbake, its plugins and its tests are provied under a GPL v3 license.
-
-flashbake licensed files:
-* flashbake/__init__.py
-* flashbake/commit.py
-* flashbkae/context.py
-* bin/flashbake
-
-plugins licensed files:
-* flashbake/plugins/__init__.py
-* flashpake/plugins/feed.py
-* flashpake/plugins/timezone.py
-* flashpake/plugins/uptime.py
-* flashpake/plugins/weather.py
-
-test licensed files:
-* test/__init__.py
-* test/plugins.py
-* plugins/hellodolly.py
-* bin/test
-
-
----- begin license block -----
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
View
1  CREDITS.txt
@@ -11,6 +11,7 @@ and working directly on the project:
* 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
View
52 bin/flashbakeall
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-
-# wrapper script for calling 'flashbake' on all projects under a given path
-
-import sys, logging
-from optparse import OptionParser
-import os
-import os.path
-import subprocess
-import fnmatch
-
-VERSION='0.23'
-
-pattern = '.flashbake'
-
-def locate_projects(root):
- for path, dirs, files in os.walk(root):
- for project_path in [os.path.normpath(path) for filename in files if fnmatch.fnmatch(filename, pattern)]:
- yield project_path
-
-
-
-if __name__ == "__main__":
- usage = "usage: %prog [options] <search_root> [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)
-
-
-
-
-
-
-
-
View
143 flashbake/__init__.py
@@ -1,16 +1,31 @@
+# copyright 2009 Thomas Gideon
#
-# __init__.py
-# Shared classes and functions for the flashbake package.
-
+# 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 <http://www.gnu.org/licenses/>.
+
+''' __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
+import glob
+import logging
import os
import os.path
-import logging
import sys
-import commands
-import glob
-from types import *
-import flashbake.plugins
-from flashbake.plugins import PluginError, PLUGIN_ERRORS
class ConfigError(Exception):
@@ -24,22 +39,54 @@ def __init__(self):
self.initialized = False
self.extra_props = dict()
- self.email = None
- self.notice_to = None
- self.notice_from = None
- self.smtp_host = 'localhost'
- self.smtp_port = 25
-
self.prop_types = dict()
- self.prop_types['smtp_port'] = int
self.plugin_names = list()
self.msg_plugins = list()
-
self.file_plugins = list()
+ self.notify_plugins = list()
self.git_path = 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, config.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:
@@ -47,17 +94,15 @@ def init(self):
self.initialized = True
- if self.notice_from == None and self.notice_to != None:
- self.notice_from = self.notice_to
-
if len(self.plugin_names) == 0:
- logging.debug('No plugins configured, enabling the stock set.')
raise ConfigError('No plugins configured!')
+ all_plugins = list()
for plugin_name in self.plugin_names:
logging.debug("initalizing plugin: %s" % plugin_name)
try:
- plugin = self.initplugin(plugin_name)
+ plugin = self.create_plugin(plugin_name)
+ all_plugins.append(plugin)
if isinstance(plugin, flashbake.plugins.AbstractMessagePlugin):
logging.debug("Message Plugin: %s" % plugin_name)
if 'flashbake.plugins.location:Location' == plugin_name:
@@ -67,6 +112,9 @@ def init(self):
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:
@@ -75,7 +123,15 @@ def init(self):
logging.warning('Skipping plugin, %s, ignorable error: %s' %
(plugin_name, e.name))
- def sharedproperty(self, name, type = None):
+ for plugin in all_plugins:
+ plugin.share_properties(self)
+
+ 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__:
@@ -96,11 +152,11 @@ def sharedproperty(self, name, type = None):
self.__dict__[name] = value
- def addplugins(self, plugin_names):
+ def add_plugins(self, plugin_names):
# use a comprehension to ensure uniqueness
[self.__add_last(inbound_name) for inbound_name in plugin_names]
- def initplugin(self, plugin_spec):
+ 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:
@@ -118,31 +174,34 @@ def initplugin(self, plugin_spec):
raise PluginError(PLUGIN_ERRORS.unknown_plugin, plugin_spec)
try:
- # TODO re-visit pkg_resources, EntryPoint
plugin_class = self.__forname(module_name, plugin_name)
plugin = plugin_class(plugin_spec)
- except:
+ except Exception, e:
+ logging.debug(e)
logging.debug('Couldn\'t load class %s' % plugin_spec)
raise PluginError(PLUGIN_ERRORS.unknown_plugin, plugin_spec)
is_message_plugin = isinstance(plugin, flashbake.plugins.AbstractMessagePlugin)
is_file_plugin = isinstance(plugin, flashbake.plugins.AbstractFilePlugin)
- if not is_message_plugin and not is_file_plugin:
+ 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)
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, 'processfiles', MethodType)
-
- plugin.init(self)
+ if is_file_plugin:
+ self.__checkattr(plugin_spec, plugin, 'notify', 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.remove(plugin_name)
self.plugin_names.append(plugin_name)
+
def __checkattr(self, plugin_spec, plugin, name, expected_type):
try:
attrib = eval('plugin.%s' % name)
@@ -152,6 +211,7 @@ def __checkattr(self, plugin_spec, plugin, name, expected_type):
if not isinstance(attrib, expected_type):
raise PluginError(PLUGIN_ERRORS.invalid_attribute, plugin_spec, name)
+
# 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):
@@ -161,6 +221,7 @@ def __forname(self, module_name, plugin_name):
classobj = getattr(module, plugin_name)
return classobj
+
class HotFiles:
"""
Track the files as they are parsed and manipulated with regards to their git
@@ -214,7 +275,7 @@ def addfile(self, filename):
self.control_files.add(rel_file)
else:
self.linked_files[expanded_file] = link
-
+
if not file_exists:
self.putabsent(filename)
@@ -262,10 +323,10 @@ def addorphans(self, git_obj, control_config):
os.remove(message_file)
- def needsnotice(self):
+ 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.outside_files) > 0)
def __check_link(self, filename):
# add, above, makes sure filename is always relative
@@ -298,3 +359,17 @@ def __drop_prefix(self, prefix, filepath):
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
View
170 flashbake/commit.py
@@ -1,29 +1,36 @@
+# copyright 2009 Thomas Gideon
#
-# commit.py
-# Parses a project's control file and wraps git operations, calling the context
-# script to build automatic commit messages as needed.
+# 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 <http://www.gnu.org/licenses/>.
+
+''' commit.py - Parses a project's control file and wraps git operations, calling the context
+script to build automatic commit messages as needed.'''
-import os
-import sys
-import re
-import datetime
-import time
-import logging
-import context
-# this import is only valid for Linux
import commands
-# Import smtplib for the actual sending function
-import smtplib
+import context
+import datetime
import flashbake
import git
+import logging
+import os
+import re
+import sys
+import time
-# Import the email modules we'll need
-if sys.hexversion < 0x2050000:
- from email.MIMEText import MIMEText
-else:
- from email.mime.text import MIMEText
-def parsecontrol(project_dir, control_file, config = None, results = None):
+def parsecontrol(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)
@@ -32,7 +39,7 @@ def parsecontrol(project_dir, control_file, config = None, results = None):
hot_files = flashbake.HotFiles(project_dir)
else:
hot_files = results
-
+
if None == config:
control_config = flashbake.ControlConfig()
else:
@@ -42,13 +49,13 @@ def parsecontrol(project_dir, control_file, config = None, results = None):
try:
for line in control_file:
# skip anything else if the config consumed the line
- if __capture(control_config, line):
+ if control_config.capture(line):
continue
hot_files.addfile(line.strip())
finally:
control_file.close()
-
+
return (hot_files, control_config)
def preparecontrol(hot_files, control_config):
@@ -58,7 +65,7 @@ def preparecontrol(hot_files, control_config):
logging.debug("running plugin %s" % plugin)
plugin.processfiles(hot_files, control_config)
return (hot_files, control_config)
-
+
def commit(control_config, hot_files, quiet_mins, dryrun):
# change to the project directory, necessary to find the .flashbake file and
# to correctly refer to the project files by relative paths
@@ -159,52 +166,16 @@ def commit(control_config, hot_files, quiet_mins, dryrun):
commit_output = git_obj.commit(message_file, to_commit)
logging.debug(commit_output)
os.remove(message_file)
+ __send_commit_notice(control_config, to_commit)
logging.info('Commit for known files complete.')
else:
logging.info('No changes to known files found to commit.')
- if hot_files.needsnotice():
- __sendnotice(control_config, hot_files)
+ if hot_files.needs_warning():
+ __send_warning(control_config, hot_files)
else:
- logging.info('No missing or untracked files found, not sending email notice.')
-
-def __capture(config, line):
- """ Used by the dot-control parsing code to capture files and properties
- as they are encountered. """
- # 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:
- config.addplugins(prop_value.split(','))
- return True
-
- # hang onto any extra propeties in case plugins use them
- if not prop_name in config.__dict__:
- config.extra_props[prop_name] = prop_value;
- return True
-
- try:
- if prop_name in config.prop_types:
- prop_value = config.prop_types[prop_name](prop_value)
- config.__dict__[prop_name] = prop_value
- except:
- raise flashbake.ConfigError(
- 'The value, %s, for option, %s, could not be parse as %s.'
- % (prop_value, prop_name, config.prop_types[prop_name]))
-
- return True
-
- return False
+ logging.info('No missing or untracked files found, not sending warning notice.')
+
def __trimgit(status_line):
if status_line.find('->') >= 0:
@@ -214,69 +185,22 @@ def __trimgit(status_line):
tokens = status_line.split(':')
return tokens[1].strip()
-def __sendnotice(control_config, hot_files):
- if (None == control_config.notice_to
+
+def __send_warning(control_config, hot_files):
+ if (len(control_config.notify_plugins) == 0
and not control_config.dryrun):
- logging.info('Skipping notice, no notice_to: recipient set.')
+ logging.info('Skipping notice, no notify plugins configured.')
return
- 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.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'
+ for plugin in control_config.notify_plugins:
+ plugin.warn(hot_files, control_config)
- 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.dryrun:
- logging.debug(body)
- if control_config.notice_to != None:
- logging.info('Dry run, skipping email notice.')
+def __send_commit_notice(control_config, to_commit):
+ if (len(control_config.notify_plugins) == 0
+ and not control_config.dryrun):
+ logging.info('Skipping notice, no notify plugins configured.')
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'] = control_config.notice_from
- msg['To'] = control_config.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'
- % (control_config.smtp_host, control_config.smtp_port))
-
- try:
- s = smtplib.SMTP()
- s.connect(host=control_config.smtp_host,port=control_config.smtp_port)
- logging.info('Sending notice to %s.' % control_config.notice_to)
- logging.debug(body)
- s.sendmail(control_config.notice_from, [control_config.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));
+ for plugin in control_config.notify_plugins:
+ plugin.notify_commit(to_commit, control_config)
View
286 bin/flashbake → flashbake/console.py
@@ -1,37 +1,143 @@
#!/usr/bin/env python
-# wrapper script that will get installed by setup.py into the execution path
-
-import sys, logging
-from optparse import OptionParser
-import os
+''' 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 <http://www.gnu.org/licenses/>.
+
+
+from flashbake.commit import commit, parsecontrol, preparecontrol
+from flashbake.context import buildmessagefile
+from flashbake.plugins import PluginError, PLUGIN_ERRORS
+from optparse import OptionParser, OptionParser
from os.path import join, dirname, exists, realpath, abspath
+import flashbake
+import flashbake.git
+import fnmatch
+import logging
+import os
+import os
+import os.path
+import subprocess
+import sys
+import sys
+import logging
-VERSION='0.25'
-def handlebadplugin(plugin_error):
- logging.debug('Plugin error, %s.' % plugin_error)
- if plugin_error.reason == PLUGIN_ERRORS.unknown_plugin:
- logging.error('Cannot load plugin, %s.' % plugin_error.plugin_spec)
- return
- if plugin_error.reason == PLUGIN_ERRORS.missing_attribute:
- logging.error('Plugin, %s, doesn\'t have the needed plugin attribute, %s.' \
- % (plugin_error.plugin_spec, plugin_error.name))
- return
+VERSION = '0.26'
+pattern = '.flashbake'
- if plugin_error.reason == PLUGIN_ERRORS.invalid_attribute:
- logging.error('Plugin, %s, has an invalid plugin attribute, %s.' \
- % (plugin_error.plugin_spec, plugin_error.name))
- return
+# entry point used by setup.py
+def main():
+ # handle options and arguments
+ parser = __build_parser()
- 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
+ (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)
+
+ # 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) = parsecontrol(project_dir, control_file, control_config, hot_files)
+ control_config.context_only = options.context_only
+ (hot_files, control_config) = preparecontrol(hot_files, control_config)
+ commit(control_config, hot_files, quiet_period, options.dryrun)
+ 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] <search_root> [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):
+ for project_path in [os.path.normpath(path) for filename in files if fnmatch.fnmatch(filename, pattern)]:
+ yield project_path
-# just provide the command line hook into the flashbake.commit module
-if __name__ == "__main__":
+
+def __build_parser():
usage = "usage: %prog [options] <project_dir> [quiet_min]"
parser = OptionParser(usage=usage, version='%s %s' % ('%prog', VERSION))
@@ -50,51 +156,10 @@ def handlebadplugin(plugin_error):
parser.add_option('-p', '--plugins', dest='plugin_dir',
action='store', type='string', metavar='PLUGIN_DIR',
help='specify an additional location for plugins')
+ return parser
- (options, args) = parser.parse_args()
-
- if options.quiet and options.verbose:
- parser.error('Cannot specify both verbose and quiet')
- level = logging.INFO
- if options.verbose:
- level = logging.DEBUG
-
- if options.quiet:
- level = logging.ERROR
-
- logging.basicConfig(level=level,
- format='%(message)s')
-
- ######################################################################
- # Setup path (borrowed directly from gwibber)
- LAUNCH_DIR = abspath(sys.path[0])
- logging.debug("Launched from %s", LAUNCH_DIR)
- source_tree_flashbake = join(LAUNCH_DIR, "..", "flashbake")
-
- # If we were invoked from a flashbake source directory add that as the
- # preferred module path ...
- if exists(join(source_tree_flashbake, "commit.py")):
- logging.info("Running from source tree; adjusting path")
- sys.path.insert(0, realpath(dirname(source_tree_flashbake)))
- try:
- import flashbake
- import flashbake.git
- from flashbake.commit import commit, parsecontrol, preparecontrol
- from flashbake.context import buildmessagefile
- from flashbake.plugins import PluginError, PLUGIN_ERRORS
- finally:
- del sys.path[0]
- else:
- logging.debug("Assuming path is correct")
- import flashbake
- import flashbake.git
- from flashbake.commit import commit, parsecontrol, preparecontrol
- from flashbake.context import buildmessagefile
- from flashbake.plugins import PluginError, PLUGIN_ERRORS
-
- home_dir = os.path.expanduser('~')
- # look for plugin directory
+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)
@@ -111,13 +176,9 @@ def handlebadplugin(plugin_error):
else:
logging.warn('Plugin directory, %s, doesn\'t exist.' % options.plugin_dir)
- if len(args) < 1:
- parser.error('Must specify project directory.')
- sys.exit(1)
- project_dir = args[0]
- # look for central config
+def __load_user_control(home_dir, project_dir):
control_file = join(home_dir, '.flashbake', 'config')
if os.path.exists(control_file):
(hot_files, control_config) = parsecontrol(project_dir, control_file)
@@ -125,7 +186,10 @@ def handlebadplugin(plugin_error):
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
@@ -134,48 +198,52 @@ def handlebadplugin(plugin_error):
if not os.path.exists(control_file):
parser.error('Could not find .flashbake or .control file in directory, "%s".' % project_dir)
- sys.exit(1)
-
-
- if options.context_only:
- try:
- (hot_files, control_config) = parsecontrol(project_dir, control_file, control_config, hot_files)
- control_config.context_only = options.context_only
- (hot_files, control_config) = preparecontrol(hot_files,control_config)
-
- msg_filename = 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)
- sys.exit(0)
- except (flashbake.git.VCError, flashbake.ConfigError), error:
- logging.error('Error: %s' % str(error))
- sys.exit(1)
- except PluginError, error:
- handlebadplugin(error)
- sys.exit(1)
+ return None
+ else:
+ return control_file
- 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)
+def __context_only(options, project_dir, control_file, control_config, hot_files):
try:
(hot_files, control_config) = parsecontrol(project_dir, control_file, control_config, hot_files)
control_config.context_only = options.context_only
- (hot_files, control_config) = preparecontrol(hot_files,control_config)
- commit(control_config, hot_files, quiet_period, options.dryrun)
+ (hot_files, control_config) = preparecontrol(hot_files, control_config)
+
+ msg_filename = 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))
- sys.exit(1)
+ return 1
except PluginError, error:
handlebadplugin(error)
- sys.exit(1)
+ return 1
+
+
+def __handle_bad_plugin(plugin_error):
+ logging.debug('Plugin error, %s.' % plugin_error)
+ if plugin_error.reason == PLUGIN_ERRORS.unknown_plugin:
+ logging.error('Cannot load plugin, %s.' % plugin_error.plugin_spec)
+ return
+
+ if plugin_error.reason == PLUGIN_ERRORS.missing_attribute:
+ 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:
+ 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
View
19 flashbake/context.py
@@ -1,6 +1,21 @@
+# copyright 2009 Thomas Gideon
#
-# context.py
-# Build up some descriptive context for automatic commit to git
+# 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 <http://www.gnu.org/licenses/>.
+
+''' context.py - Build up some descriptive context for automatic commit to git'''
import sys
import os
View
21 flashbake/git.py
@@ -1,7 +1,22 @@
+# copyright 2009 Thomas Gideon
#
-# git.py
-# Wrap the call outs to git, adding sanity checks and environment set up if
-# needed.
+# 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 <http://www.gnu.org/licenses/>.
+
+''' git.py - Wrap the call outs to git, adding sanity checks and environment set up if
+needed.'''
import os
import logging
View
122 flashbake/plugins/__init__.py
@@ -1,5 +1,23 @@
-# from http://pypi.python.org/pypi/enum/
+# 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 <http://www.gnu.org/licenses/>.
+
from enum import Enum
+import logging
+
PLUGIN_ERRORS = Enum(
'invalid_plugin',
'invalid_type',
@@ -10,8 +28,9 @@
'ignorable_error'
)
+
class PluginError(Exception):
- def __init__(self, reason, plugin_spec, name = None):
+ def __init__(self, reason, plugin_spec, name=None):
self.plugin_spec = plugin_spec
self.reason = reason
self.name = name
@@ -20,30 +39,73 @@ def __str__(self):
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:
+ service_name, property_prefix = service_and_prefix(plugin_spec)
+ 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 requireproperty(self, config, name, type = None):
- """ Useful to plugins to express a property that is required in the
- dot-control file and to move it from the extra_props dict to a
- property of the config. """
- if not name in config.extra_props:
- raise PluginError(PLUGIN_ERRORS.missing_property, self.plugin_spec, name)
- self.optionalproperty(config, name)
-
- def optionalproperty(self, config, name, type = None):
+ def __capture_property(self, config, name, type=None, required=False, default=None):
""" Move a property, if present, from the ControlConfig to the daughter
plugin. """
- value = None
+ 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 name in config.extra_props:
- value = config.extra_props[name]
- del config.extra_props[name]
+ 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:
@@ -54,32 +116,50 @@ def optionalproperty(self, config, name, type = None):
% (value, name, type))
self.__dict__[name] = value
- def abstract(self):
+
+ 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):
+ def __init__(self, plugin_spec, connectable=False):
+ AbstractPlugin.__init__(self, plugin_spec)
self.connectable = connectable
- self.plugin_spec = plugin_spec
+
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 __init__(self, plugin_spec):
- self.plugin_spec = plugin_spec
-
def processfiles(self, hot_files, config):
""" This method is required, it will asplode if not overridden by
daughter classes. """
self.abstract()
+
+
+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):
+ ''' Option method to notify when a commit is performed, probably most useful
+ for services like desktop notifiers. '''
+ pass
View
49 flashbake/plugins/feed.py
@@ -1,44 +1,55 @@
+# copyright 2009 Thomas Gideon
#
-# feed.py
-# Stock plugin that pulls latest n items from a feed by a given author.
+# 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 <http://www.gnu.org/licenses/>.
+
+''' 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)
-
- def init(self, config):
- """ Grab any extra properties that the config parser found and are
- needed by this module. """
- self.requireproperty(config, 'feed_url')
- self.optionalproperty(config, 'feed_author')
- self.optionalproperty(config, 'feed_limit', int)
- if self.feed_limit == None:
- self.feed_limit = 5
+ 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()
+ (title, last_items) = self.__fetchfeed()
if len(last_items) > 0:
- if self.feed_author == None:
+ 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.feed_author})
+ % {'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.feed_url)
+ message_file.write('Couldn\'t fetch entries from feed, %s.\n' % self.url)
return len(last_items) > 0
@@ -47,7 +58,7 @@ def __fetchfeed(self):
creator. """
try:
- feed = feedparser.parse(self.feed_url)
+ feed = feedparser.parse(self.url)
if not 'title' in feed.feed:
logging.info('Feed title is empty, feed is either malformed or unavailable.')
@@ -57,13 +68,13 @@ def __fetchfeed(self):
by_creator = []
for entry in feed.entries:
- if self.feed_author != None and entry.author != self.feed_author:
+ 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.feed_limit <= len(by_creator):
+ if self.limit <= len(by_creator):
break
return (feed_title, by_creator)
View
153 flashbake/plugins/location.py
@@ -1,153 +0,0 @@
-# location.py
-# Net location plugin.
-
-import logging
-import urllib
-import urllib2
-import re
-from urllib2 import HTTPError, URLError
-from flashbake.plugins import AbstractMessagePlugin
-from xml.dom import minidom
-import os
-import os.path
-
-class Location(AbstractMessagePlugin):
- def __init__(self, plugin_spec):
- AbstractMessagePlugin.__init__(self, plugin_spec, True)
-
- def init(self, config):
- """ Shares the timezone_tz: property with timezone:TimeZone and supports
- an optional weather_city: property. """
- config.sharedproperty('location_location')
- config.sharedproperty('location_data')
-
- 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_data = 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://iplocationtools.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('Failed with 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 == None:
- message_file.write('Failed to parse Hello with public IP address.')
- logging.debug(hello_line)
- m = re.search('([0-9]+\.){3}([0-9]+){1}', hello_line)
- if m == None:
- message_file.write('Failed to parse Hello with public IP address.')
- 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('Failed with reason %s.' % e.reason)
- return None
View
114 flashbake/plugins/mail.py
@@ -0,0 +1,114 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+'''
+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
+else:
+ from email.mime.text import MIMEText
+
+
+
+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.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.dryrun:
+ 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));
View
117 flashbake/plugins/microblog.py
@@ -1,17 +1,35 @@
-# microblog.py
-# by Ben Snider, bensnider.com
+# 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 <http://www.gnu.org/licenses/>.
+
+
+
+''' microblog.py - microblog plugin by Ben Snider, bensnider.com '''
import logging, urllib
from urllib2 import HTTPError, URLError
from xml.etree.ElementTree import ElementTree
from flashbake.plugins import AbstractMessagePlugin
+
+
class Twitter(AbstractMessagePlugin):
-
+
def __init__(self, plugin_spec):
AbstractMessagePlugin.__init__(self, plugin_spec, True)
- self.service_name = plugin_spec.split(':')[-1]
- self.property_prefix = '_'.join(self.service_name.lower().strip().split(' '))
self.service_url = 'http://twitter.com'
self.optional_field_info = { \
'source':{'path':'source', 'transform':propercase}, \
@@ -19,47 +37,33 @@ def __init__(self, plugin_spec):
'favorited':{'path':'favorited', 'transform':propercase}, \
'tweeted_on': {'path':'created_at', 'transform':utc_to_local}, \
}
- self.requiredproperties = [self.property_prefix+'_user']
- self.optionalproperties = [self.property_prefix+'_limit', self.property_prefix+'_optional_fields']
-
+ self.define_property('user', required=True)
+ self.define_property('limit', int, False, 5)
+ self.define_property('optional_fields')
+
+
def init(self, config):
- for prop in self.requiredproperties:
- self.requireproperty(config, prop)
- for prop in self.optionalproperties:
- self.optionalproperty(config, prop)
-
- self.__formatproperties(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.__dict__[self.property_prefix+'_user'], \
- 'limit':self.__dict__[self.property_prefix+'_limit']} \
-
- def __formatproperties(self, config):
- limit_property_name = self.property_prefix+'_limit'
-
- # short circuit this conditional so we don't get a TypeError
- if (self.__dict__[limit_property_name] == None or int(self.__dict__[limit_property_name]) < 1):
- self.__dict__[limit_property_name] = 5
- else:
- self.__dict__[limit_property_name] = int(self.__dict__[limit_property_name])
- # respect the twitter api limits
- if (self.__dict__[limit_property_name] > 200):
- logging.warn('Please use a limit <= 200.');
- self.__dict__[limit_property_name] = 200
-
+ 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):
- optional_fields_name = self.property_prefix+'_optional_fields'
-
# We don't have to worry about a KeyError here since this property
# should have been set to None by self.setoptionalproperty.
- if (self.__dict__[optional_fields_name] == None):
- self.__dict__[optional_fields_name] = []
+ if (self.optional_fields == None):
+ self.optional_fields = []
else:
# get the optional fields, split on commas
- fields = self.__dict__[optional_fields_name].strip().split(',')
+ fields = self.optional_fields.strip().split(',')
newFields = []
for field in fields:
field = field.strip()
@@ -69,30 +73,31 @@ def __setoptionalfields(self, config):
newFields.append(field)
# finally sort the list so its the same each run, provided the config is the same
newFields.sort()
- self.__dict__[optional_fields_name] = newFields
-
+ 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
+
+ i = 1
for item in last_tweets:
to_file += ('%d) %s\n' % (i, item['tweet']))
- for field in self.__dict__[self.property_prefix+'_optional_fields']:
+ for field in self.optional_fields:
to_file += ('\t%s: %s\n' % (propercase(field), item[field]))
- i+=1
-
+ 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
@@ -100,7 +105,7 @@ def __fetchitems(self, config):
any optional fields. The
'''
results = [None, []]
-
+
try:
twitter_xml = urllib.urlopen(self.twitter_url)
except HTTPError, e:
@@ -112,31 +117,31 @@ def __fetchitems(self, config):
except IOError:
logging.error('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.__dict__[self.property_prefix+'_optional_fields']:
+ 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'
@@ -150,7 +155,7 @@ def propercase(string):
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, time, datetime
View
50 flashbake/plugins/music.py
@@ -1,6 +1,21 @@
+# copyright 2009 Thomas Gideon
#
-# music.py
-# Stock plugin to calculate the system's uptime and add to the commit message.
+# 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 <http://www.gnu.org/licenses/>.
+
+''' music.py - Plugin for gathering last played tracks from music player. '''
import sqlite3
import os.path
@@ -8,20 +23,17 @@
import time
from flashbake.plugins import AbstractMessagePlugin
+
+
class Banshee(AbstractMessagePlugin):
- def init(self, config):
+ def __init__(self, plugin_spec):
""" Add an optional property for specifying a different location for the
- Banshee database. """
- self.optionalproperty(config, 'banshee_db')
- self.optionalproperty(config, 'banshee_limit', int)
- self.optionalproperty(config, 'banshee_last_played_format')
- if self.banshee_db == None:
- logging.debug('Using default location for Banshee database.')
- self.banshee_db = os.path.join(os.path.expanduser('~'),
- '.config', 'banshee-1', 'banshee.db')
- if self.banshee_limit == None:
- logging.debug('Using default limit of 3 most recent tracks.')
- self.banshee_limit = 3
+ 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. """
@@ -31,8 +43,8 @@ def addcontext(self, message_file, config):
join CoreArtists a on t.ArtistID = a.ArtistID
order by LastPlayedStamp desc
limit %d"""
- query = query.strip() % self.banshee_limit
- conn = sqlite3.connect(self.banshee_db)
+ query = query.strip() % self.limit
+ conn = sqlite3.connect(self.db)
try:
cursor = conn.cursor()
logging.debug('Executing %s' % query)
@@ -41,9 +53,9 @@ def addcontext(self, message_file, config):
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.banshee_last_played_format != None:
- logging.debug('Using format %s' % self.banshee_last_played_format)
- last_played = time.strftime(self.banshee_last_played_format,
+ 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])
View
131 flashbake/plugins/scrivener.py
@@ -1,47 +1,56 @@
-# scrivener.py -- Scrivener flashbake plugin
-# by Jason Penney, jasonpenney.net
+# 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 <http://www.gnu.org/licenses/>.
+
+''' scrivener.py - Scrivener flashbake plugin
+by Jason Penney, jasonpenney.net'''
-import logging, flashbake, flashbake.plugins, fnmatch, os
-import subprocess, glob
-import pickle
from flashbake.plugins import *
+import logging
+import flashbake
+import flashbake.plugins
+import fnmatch
+import os
+import pickle
+import subprocess
+import glob
-## find_executable and executable_available could possibly be useful elsewhere
-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
-
-
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'):
+ 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 Error("unable to calculate paths")
- if os.path.samefile(path,start):
+ if os.path.samefile(path, start):
return "."
if not start.endswith(os.path.sep):
@@ -49,24 +58,25 @@ def _relpath(path, start):
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)):
+ contents = list()
+ for path, dirs, files in os.walk(os.path.join(hot_files.project_dir, scrivener_project)):
if hasattr(os.path, "relpath"):
- rpath = os.path.relpath(path,hot_files.project_dir)
+ rpath = os.path.relpath(path, hot_files.project_dir)
else:
try:
import pathutils
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))
+ 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))
@@ -74,41 +84,42 @@ def get_logfile_name(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 init(self,config):
- config.sharedproperty('scrivener_projects')
-
def processfiles(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):
+ for hotfile in find_scrivener_project_contents(hot_files, f):
#logging.debug(" - %s" % hotfile)
hot_files.control_files.add(hotfile)
+
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 executable_available('textutil'):
+
+ def init(self, config):
+ if not flashbake.executable_available('textutil'):
raise PluginError(PLUGIN_ERRORS.ignorable_error, self.plugin_spec, 'Could not find command, textutil.')
- config.sharedproperty('scrivener_projects')
- config.sharedproperty('scrivener_project_count')
-
+
+
def processfiles(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)
+ 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)
+ 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')
+ log = open(logfile, 'r')
oldCount = pickle.load(log)
log.close()
else:
@@ -123,44 +134,44 @@ def processfiles(self, hot_files, config):
'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'])
+ '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 = 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):
+ def get_count(self, file, matches):
count = 0
- args = ['textutil','-stdout','-cat', 'txt']
- do_count=False
+ 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