Add --notify to `be serve`.
authorW. Trevor King <wking@drexel.edu>
Sat, 16 Apr 2011 21:08:35 +0000 (17:08 -0400)
committerW. Trevor King <wking@drexel.edu>
Sat, 16 Apr 2011 21:08:35 +0000 (17:08 -0400)
NEWS
libbe/command/serve.py
libbe/storage/http.py
libbe/util/subproc.py

diff --git a/NEWS b/NEWS
index a93d47e5a8ae9729c31925b6544ceb2ebec321ac..986e88708714218f128b67e59c906625e0f91ff8 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,7 @@
 April 16, 2011
  * Added --preserve-uuids to `be import-xml`.
  * Added --assigned, --severity, and --status to `be new`.
+ * Added --notify to `be serve`.
 
 March 5, 2011
  * Release version 1.0.1 (bugfixes).
index ba4b0d8526dea9776c3fd080894482db16e815f0..b31113928540290fe16955cf872df42581a52496 100644 (file)
@@ -58,6 +58,7 @@ import libbe
 import libbe.command
 import libbe.command.util
 import libbe.util.encoding
+import libbe.util.subproc
 import libbe.version
 
 if libbe.TESTING == True:
@@ -507,9 +508,10 @@ class ServerApp (WSGI_AppObject):
     """
     server_version = "BE-server/" + libbe.version.version()
 
-    def __init__(self, storage, *args, **kwargs):
-        WSGI_AppObject.__init__(self, *args, **kwargs)
+    def __init__(self, storage, notify=False, **kwargs):
+        WSGI_AppObject.__init__(self, **kwargs)
         self.storage = storage
+        self.notify = notify
         self.http_user_error = 418
 
         self.urls = [
@@ -570,6 +572,9 @@ class ServerApp (WSGI_AppObject):
         directory = self.data_get_boolean(
             data, 'directory', default=False, source=source)
         self.storage.add(id, parent=parent, directory=directory)
+        if self.notify:
+            self._notify(environ, 'add', id,
+                         [('parent', parent), ('directory', directory)])
         return self.ok_response(environ, start_response, None)
 
     def exists(self, environ, start_response):
@@ -593,6 +598,8 @@ class ServerApp (WSGI_AppObject):
             self.storage.recursive_remove(id)
         else:
             self.storage.remove(id)
+        if self.notify:
+            self._notify(environ, 'remove', id, [('recursive', recursive)])
         return self.ok_response(environ, start_response, None)
 
     def ancestors(self, environ, start_response):
@@ -641,6 +648,8 @@ class ServerApp (WSGI_AppObject):
             raise _HandlerError(406, 'Missing query key value')
         value = data['value']
         self.storage.set(id, value)
+        if self.notify:
+            self._notify(environ, 'set', id, [('value', value)])
         return self.ok_response(environ, start_response, None)
 
     def commit(self, environ, start_response):
@@ -661,6 +670,10 @@ class ServerApp (WSGI_AppObject):
             revision = self.storage.commit(summary, body, allow_empty)
         except libbe.storage.EmptyCommit, e:
             raise _HandlerError(self.http_user_error, 'EmptyCommit')
+        if self.notify:
+            self._notify(environ, 'commit', id,
+                         [('allow_empty', allow_empty), ('summary', summary),
+                          ('body', body)])
         return self.ok_response(environ, start_response, revision)
 
     def revision_id(self, environ, start_response):
@@ -700,6 +713,35 @@ class ServerApp (WSGI_AppObject):
                     raise _Unauthorized() # only non-guests allowed to write
             # allow read-only commands for all users
 
+    def _notify(self, environ, command, id, params):
+        message = self._format_notification(environ, command, id, params)
+        self._submit_notification(message)
+
+    def _format_notification(self, environ, command, id, params):
+        key_length = len('command')
+        for key,value in params:
+            if len(key) > key_length and '\n' not in str(value):
+                key_length = len(key)
+        key_length += 1
+        lines = []
+        multi_line_params = []
+        for key,value in [('address', environ.get('REMOTE_ADDR', '-')),
+                          ('command', command), ('id', id)]+params:
+            v = str(value)
+            if '\n' in v:
+                multi_line_params.append((key,v))
+                continue
+            lines.append('%*.*s %s' % (key_length, key_length, key+':', v))
+        lines.append('')
+        for key,value in multi_line_params:
+            lines.extend(['=== START %s ===' % key, v,
+                          '=== STOP %s ===' % key, ''])
+        lines.append('')
+        return '\n'.join(lines)
+
+    def _submit_notification(self, message):
+        libbe.util.subproc.invoke(self.notify, stdin=message, shell=True)
+
 
 class Serve (libbe.command.Command):
     """:class:`~libbe.command.base.Command` wrapper around
@@ -721,6 +763,10 @@ class Serve (libbe.command.Command):
                         name='host', metavar='HOST', default='')),
                 libbe.command.Option(name='read-only', short_name='r',
                     help='Dissable operations that require writing'),
+                libbe.command.Option(name='notify', short_name='n',
+                    help='Send notification emails for changes.',
+                    arg=libbe.command.Argument(
+                        name='notify', metavar='EMAIL-COMMAND', default=None)),
                 libbe.command.Option(name='ssl', short_name='s',
                     help='Use CherryPy to serve HTTPS (HTTP over SSL/TLS)'),
                 libbe.command.Option(name='auth', short_name='a',
@@ -742,7 +788,8 @@ class Serve (libbe.command.Command):
             self._check_restricted_access(storage, params['auth'])
         users = Users(params['auth'])
         users.load()
-        app = ServerApp(storage=storage, logger=self.logger)
+        app = ServerApp(
+            storage=storage, notify=params['notify'], logger=self.logger)
         if params['auth'] != None:
             app = AdminApp(app, users=users, logger=self.logger)
             app = AuthenticationApp(app, realm=storage.repo,
@@ -860,6 +907,7 @@ if libbe.TESTING == True:
             self.logger.setLevel(logging.INFO)
             self.default_environ = { # required by PEP 333
                 'REQUEST_METHOD': 'GET', # 'POST', 'HEAD'
+                'REMOTE_ADDR': '192.168.0.123',
                 'SCRIPT_NAME':'',
                 'PATH_INFO': '',
                 #'QUERY_STRING':'',   # may be empty or absent
index fe5bbc8c1a756b90398eda520a0d9a3e2623cb4e..ee589a2d7ffaf0c5b362ac0126e637a72d80ee22 100644 (file)
@@ -358,6 +358,7 @@ if TESTING == True:
             # duplicated from libbe.command.serve.WSGITestCase
             self.default_environ = {
                 'REQUEST_METHOD': 'GET', # 'POST', 'HEAD'
+                'REMOTE_ADDR': '192.168.0.123',
                 'SCRIPT_NAME':'',
                 'PATH_INFO': '',
                 #'QUERY_STRING':'',   # may be empty or absent
index 412ed3621256f451a5563015da4da8e9954f00a3..be3bf31df84201c7806dab478509a6b71f03a063 100644 (file)
@@ -21,6 +21,7 @@ Functions for running external commands in subprocesses.
 
 from subprocess import Popen, PIPE
 import sys
+import types
 
 import libbe
 from encoding import get_encoding
@@ -45,7 +46,8 @@ class CommandError(Exception):
         self.stderr = stderr
 
 def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
-           cwd=None, unicode_output=True, verbose=False, encoding=None):
+           cwd=None, shell=None, unicode_output=True, verbose=False,
+           encoding=None):
     """
     expect should be a tuple of allowed exit codes.  cwd should be
     the directory from which the command will be executed.  When
@@ -54,18 +56,29 @@ def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
     """
     if cwd == None:
         cwd = '.'
+    if isinstance(shell, types.StringTypes):
+        list_args = ' '.split(args)  # sloppy, but just for logging
+        str_args = args
+    else:
+        list_args = args
+        str_args = ' '.join(args)  # sloppy, but just for logging
     if verbose == True:
-        print >> sys.stderr, '%s$ %s' % (cwd, ' '.join(args))
+        print >> sys.stderr, '%s$ %s' % (cwd, str_args)
     try :
         if _POSIX:
-            q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr, cwd=cwd)
+            if shell is None:
+                shell = False
+            q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr,
+                      shell=shell, cwd=cwd)
         else:
             assert _MSWINDOWS==True, 'invalid platform'
+            if shell is None:
+                shell = True
             # win32 don't have os.execvp() so have to run command in a shell
             q = Popen(args, stdin=PIPE, stdout=stdout, stderr=stderr,
-                      shell=True, cwd=cwd)
+                      shell=shell, cwd=cwd)
     except OSError, e:
-        raise CommandError(args, status=e.args[0], stderr=e)
+        raise CommandError(list_args, status=e.args[0], stderr=e)
     stdout,stderr = q.communicate(input=stdin)
     status = q.wait()
     if unicode_output == True:
@@ -78,7 +91,7 @@ def invoke(args, stdin=None, stdout=PIPE, stderr=PIPE, expect=(0,),
     if verbose == True:
         print >> sys.stderr, '%d\n%s%s' % (status, stdout, stderr)
     if status not in expect:
-        raise CommandError(args, status, stdout, stderr)
+        raise CommandError(list_args, status, stdout, stderr)
     return status, stdout, stderr
 
 class Pipe (object):