Compare commits
18 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a0b35dfadb | ||
![]() |
8fabdf8b72 | ||
![]() |
73978ffeb7 | ||
![]() |
183530afc9 | ||
![]() |
8a0f300fc0 | ||
![]() |
ee9c6622ff | ||
![]() |
4fe5ad9bf0 | ||
![]() |
25611f1e68 | ||
![]() |
4ed6b09a24 | ||
![]() |
3766dbfff1 | ||
![]() |
3bfda431fa | ||
![]() |
4dc4f3f1ec | ||
![]() |
2e57936ed1 | ||
![]() |
33c951e7fe | ||
![]() |
9262b598ac | ||
![]() |
6c517a9126 | ||
![]() |
ea7473f18d | ||
![]() |
896a320da5 |
21 changed files with 387 additions and 172 deletions
44
CHANGES.txt
44
CHANGES.txt
|
@ -1,4 +1,48 @@
|
|||
|
||||
0.1.2
|
||||
-----
|
||||
|
||||
* Allow config file to prevent logging configuration from happening.
|
||||
|
||||
|
||||
0.1.1
|
||||
-----
|
||||
|
||||
* Some random things required in production at MaMa Jean's...
|
||||
|
||||
Specifically this is known to replace occurrences of e.g. ``edbob.User`` with
|
||||
a more standard (properly imported) reference to ``User``.
|
||||
|
||||
|
||||
0.1a29
|
||||
------
|
||||
|
||||
* Removed ``setup.cfg`` file.
|
||||
|
||||
* Changed some logging instances from ``INFO`` to ``DEBUG``.
|
||||
|
||||
* Updated ``repr()`` output for model classes.
|
||||
|
||||
|
||||
0.1a28
|
||||
------
|
||||
|
||||
- [bug] Secured daemon PID files. They are no longer readable or writeable
|
||||
(whoops) by anyone other than the user who owns the process.
|
||||
|
||||
- [feature] Added ``--progress`` argument to command system.
|
||||
|
||||
- [general] Added initial Fabric script.
|
||||
|
||||
|
||||
0.1a27
|
||||
------
|
||||
|
||||
- [feature] Overhauled file monitors. Added the "process existing" (defaults
|
||||
to true) and "stop on error" (defaults to false) features to both Linux and
|
||||
Win32 file monitors. Both features may be overridden in config. The Linux
|
||||
file monitor was rewritten as an ``edbob.daemon.Daemon`` class.
|
||||
|
||||
0.1a26
|
||||
------
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = '0.1a26'
|
||||
__version__ = '0.1.2'
|
||||
|
|
|
@ -105,6 +105,7 @@ Options:
|
|||
Config path (may be specified more than once)
|
||||
-n, --no-init Don't load config before executing command
|
||||
-d, --debug Increase logging level to DEBUG
|
||||
-P, --progress Show progress indicators (where relevant)
|
||||
-v, --verbose Increase logging level to INFO
|
||||
-V, --version Display program version and exit
|
||||
|
||||
|
@ -132,6 +133,7 @@ Try '%(name)s help <command>' for more help.""" % self
|
|||
metavar='PATH')
|
||||
parser.add_argument('-d', '--debug', action='store_true', dest='debug')
|
||||
parser.add_argument('-n', '--no-init', action='store_true', default=False)
|
||||
parser.add_argument('-P', '--progress', action='store_true', default=False)
|
||||
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose')
|
||||
parser.add_argument('-V', '--version', action='version',
|
||||
version="%%(prog)s %s" % self.version)
|
||||
|
@ -182,6 +184,7 @@ Try '%(name)s help <command>' for more help.""" % self
|
|||
|
||||
# And finally, do something of real value...
|
||||
cmd = self.subcommands[cmd](parent=self)
|
||||
cmd.show_progress = args.progress
|
||||
cmd._run(*(args.command + args.argv))
|
||||
|
||||
|
||||
|
@ -432,11 +435,6 @@ class FileMonitorCommand(Subcommand):
|
|||
uninstall = subparsers.add_parser('uninstall', help="Uninstall (remove) service")
|
||||
uninstall.set_defaults(subcommand='remove')
|
||||
|
||||
else:
|
||||
parser.add_argument('-D', '--dont-daemonize',
|
||||
action='store_false', dest='daemonize',
|
||||
help="Don't daemonize when starting")
|
||||
|
||||
def get_win32_module(self):
|
||||
from edbob.filemon import win32
|
||||
return win32
|
||||
|
@ -454,10 +452,10 @@ class FileMonitorCommand(Subcommand):
|
|||
from edbob.filemon import linux as filemon
|
||||
|
||||
if args.subcommand == 'start':
|
||||
filemon.start_daemon(self.appname, daemonize=args.daemonize)
|
||||
filemon.start_daemon(self.appname)
|
||||
|
||||
elif args.subcommand == 'stop':
|
||||
filemon.stop_daemon()
|
||||
filemon.stop_daemon(self.appname)
|
||||
|
||||
elif sys.platform == 'win32':
|
||||
from edbob import win32
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# This code was stolen from:
|
||||
# This code was (mostly, with some tweaks) stolen from:
|
||||
# http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
|
||||
|
||||
import sys, os, time, atexit
|
||||
import stat
|
||||
from signal import SIGTERM
|
||||
|
||||
class Daemon:
|
||||
|
@ -65,6 +66,7 @@ class Daemon:
|
|||
atexit.register(self.delpid)
|
||||
pid = str(os.getpid())
|
||||
file(self.pidfile,'w+').write("%s\n" % pid)
|
||||
os.chmod(self.pidfile, stat.S_IRUSR|stat.S_IWUSR)
|
||||
|
||||
def delpid(self):
|
||||
os.remove(self.pidfile)
|
||||
|
|
|
@ -366,7 +366,7 @@ def extend_framework():
|
|||
session.close()
|
||||
|
||||
for name in sorted(extensions, extension_sorter(extensions)):
|
||||
log.info("Applying active extension: %s" % name)
|
||||
log.debug("Applying active extension: %s" % name)
|
||||
ext = extensions[name]
|
||||
# merge_extension_metadata(ext)
|
||||
# ext.extend_classes()
|
||||
|
|
|
@ -51,7 +51,8 @@ class Permission(Base):
|
|||
permission = Column(String(50), primary_key=True)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Permission: %s, %s>" % (self.role, self.permission)
|
||||
return "Permission(role_uuid={0}, permission={1})".format(
|
||||
repr(self.role_uuid), repr(self.permission))
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.permission or '')
|
||||
|
@ -69,7 +70,7 @@ class UserRole(Base):
|
|||
role_uuid = Column(String(32), ForeignKey('roles.uuid'))
|
||||
|
||||
def __repr__(self):
|
||||
return "<UserRole: %s : %s>" % (self.user, self.role)
|
||||
return "UserRole(uuid={0})".format(repr(self.uuid))
|
||||
|
||||
|
||||
class Role(Base):
|
||||
|
@ -97,7 +98,7 @@ class Role(Base):
|
|||
getset_factory=getset_factory)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Role: %s>" % self.name
|
||||
return "Role(uuid={0})".format(repr(self.uuid))
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.name or '')
|
||||
|
@ -124,7 +125,7 @@ class User(Base):
|
|||
getset_factory=getset_factory)
|
||||
|
||||
def __repr__(self):
|
||||
return "<User: %s>" % self.username
|
||||
return "User(uuid={0})".format(repr(self.uuid))
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.username or '')
|
||||
|
|
|
@ -72,7 +72,8 @@ class PhoneNumber(Base):
|
|||
__mapper_args__ = {'polymorphic_on': parent_type}
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: %s>" % (self.__class__.__name__, self.number)
|
||||
return "{0}(uuid={1})".format(
|
||||
self.__class__.__name__, repr(self.uuid))
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.number)
|
||||
|
@ -103,7 +104,8 @@ class EmailAddress(Base):
|
|||
__mapper_args__ = {'polymorphic_on': parent_type}
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s: %s>" % (self.__class__.__name__, self.address)
|
||||
return "{0}(uuid={1})".format(
|
||||
self.__class__.__name__, repr(self.uuid))
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.address)
|
||||
|
@ -131,7 +133,7 @@ class Person(Base):
|
|||
display_name = Column(String(100), default=get_person_display_name)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Person: %s>" % self.display_name
|
||||
return "Person(uuid={0})".format(repr(self.uuid))
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.display_name or '')
|
||||
|
|
|
@ -54,7 +54,7 @@ class ActiveExtension(Base):
|
|||
name = Column(String(50), primary_key=True)
|
||||
|
||||
def __repr__(self):
|
||||
return "<ActiveExtension: %s>" % self.name
|
||||
return "ActiveExtension(name={0})".format(repr(self.name))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name or '')
|
||||
|
@ -71,4 +71,4 @@ class Setting(Base):
|
|||
value = Column(Text)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Setting: %s>" % self.name
|
||||
return "Setting(name={0})".format(repr(self.name))
|
||||
|
|
|
@ -26,10 +26,18 @@
|
|||
``edbob.filemon`` -- File Monitoring Service
|
||||
"""
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import Queue
|
||||
import logging
|
||||
|
||||
import edbob
|
||||
from edbob.errors import email_exception
|
||||
|
||||
if sys.platform == 'win32':
|
||||
import win32api
|
||||
from edbob.win32 import file_is_free
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -65,6 +73,12 @@ class MonitorProfile(object):
|
|||
self.locks = edbob.config.getboolean(
|
||||
'%s.filemon' % appname, '%s.locks' % key, default=False)
|
||||
|
||||
self.process_existing = edbob.config.getboolean(
|
||||
'%s.filemon' % appname, '%s.process_existing' % key, default=True)
|
||||
|
||||
self.stop_on_error = edbob.config.getboolean(
|
||||
'%s.filemon' % appname, '%s.stop_on_error' % key, default=False)
|
||||
|
||||
|
||||
def get_monitor_profiles(appname):
|
||||
"""
|
||||
|
@ -110,3 +124,105 @@ def get_monitor_profiles(appname):
|
|||
del monitored[key]
|
||||
|
||||
return monitored
|
||||
|
||||
|
||||
def queue_existing(profile, path):
|
||||
"""
|
||||
Adds files found in a watched folder to a processing queue. This is called
|
||||
when the monitor first starts, to handle the case of files which exist
|
||||
prior to startup.
|
||||
|
||||
If files are found, they are first sorted by modification timestamp, using
|
||||
a lexical sort on the filename as a tie-breaker, and then added to the
|
||||
queue in that order.
|
||||
|
||||
:param profile: Monitor profile for which the folder is to be watched. The
|
||||
profile is expected to already have a queue attached; any existing files
|
||||
will be added to this queue.
|
||||
:type profile: :class:`edbob.filemon.MonitorProfile` instance
|
||||
|
||||
:param path: Folder path which is to be checked for files.
|
||||
:type path: string
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
|
||||
def sorter(x, y):
|
||||
mtime_x = os.path.getmtime(x)
|
||||
mtime_y = os.path.getmtime(y)
|
||||
if mtime_x < mtime_y:
|
||||
return -1
|
||||
if mtime_x > mtime_y:
|
||||
return 1
|
||||
return cmp(x, y)
|
||||
|
||||
paths = [os.path.join(path, x) for x in os.listdir(path)]
|
||||
for path in sorted(paths, cmp=sorter):
|
||||
|
||||
# Only process normal files.
|
||||
if not os.path.isfile(path):
|
||||
continue
|
||||
|
||||
# If using locks, don't process "in transit" files.
|
||||
if profile.locks and path.endswith('.lock'):
|
||||
continue
|
||||
|
||||
log.debug("queue_existing: queuing existing file for "
|
||||
"profile '%s': %s" % (profile.key, path))
|
||||
profile.queue.put(path)
|
||||
|
||||
|
||||
def perform_actions(profile):
|
||||
"""
|
||||
Callable target for action threads.
|
||||
"""
|
||||
|
||||
keep_going = True
|
||||
while keep_going:
|
||||
|
||||
try:
|
||||
path = profile.queue.get_nowait()
|
||||
except Queue.Empty:
|
||||
pass
|
||||
else:
|
||||
|
||||
# In some cases, processing one file may cause other related files
|
||||
# to also be processed. When this happens, a path on the queue may
|
||||
# point to a file which no longer exists.
|
||||
if not os.path.exists(path):
|
||||
log.info("perform_actions: path does not exist: %s" % path)
|
||||
continue
|
||||
|
||||
log.debug("perform_actions: processing file: %s" % path)
|
||||
|
||||
if sys.platform == 'win32':
|
||||
while not file_is_free(path):
|
||||
win32api.Sleep(0)
|
||||
|
||||
for spec, func, args in profile.actions:
|
||||
|
||||
log.info("perform_actions: calling function '%s' on file: %s" %
|
||||
(spec, path))
|
||||
|
||||
try:
|
||||
func(path, *args)
|
||||
|
||||
except:
|
||||
log.exception("perform_actions: exception occurred "
|
||||
"while processing file: %s" % path)
|
||||
email_exception()
|
||||
|
||||
# Don't process any more files if the profile is so
|
||||
# configured.
|
||||
if profile.stop_on_error:
|
||||
keep_going = False
|
||||
|
||||
# Either way this particular file probably shouldn't be
|
||||
# processed any further.
|
||||
log.warning("perform_actions: no further processing "
|
||||
"will be done for file: %s" % path)
|
||||
break
|
||||
|
||||
log.warning("perform_actions: error encountered, and configuration "
|
||||
"dictates that no more actions will be processed for "
|
||||
"profile: %s" % profile.key)
|
||||
|
|
|
@ -27,14 +27,24 @@
|
|||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import os.path
|
||||
import signal
|
||||
import threading
|
||||
import Queue
|
||||
import logging
|
||||
|
||||
try:
|
||||
import pyinotify
|
||||
except ImportError:
|
||||
# Mock out for testing on Windows.
|
||||
class Dummy(object):
|
||||
pass
|
||||
pyinotify = Dummy()
|
||||
pyinotify.ProcessEvent = Dummy
|
||||
|
||||
import edbob
|
||||
from edbob.filemon import get_monitor_profiles
|
||||
from edbob import filemon
|
||||
from edbob.daemon import Daemon
|
||||
from edbob.errors import email_exception
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -45,9 +55,8 @@ class EventHandler(pyinotify.ProcessEvent):
|
|||
Event processor for file monitor daemon.
|
||||
"""
|
||||
|
||||
def my_init(self, actions=[], locks=False, **kwargs):
|
||||
self.actions = actions
|
||||
self.locks = locks
|
||||
def my_init(self, profile=None, **kwargs):
|
||||
self.profile = profile
|
||||
|
||||
def process_IN_ACCESS(self, event):
|
||||
log.debug("EventHandler: IN_ACCESS: %s" % event.pathname)
|
||||
|
@ -57,87 +66,85 @@ class EventHandler(pyinotify.ProcessEvent):
|
|||
|
||||
def process_IN_CLOSE_WRITE(self, event):
|
||||
log.debug("EventHandler: IN_CLOSE_WRITE: %s" % event.pathname)
|
||||
if not self.locks:
|
||||
self.perform_actions(event.pathname)
|
||||
if not self.profile.locks:
|
||||
self.profile.queue.put(event.pathname)
|
||||
|
||||
def process_IN_CREATE(self, event):
|
||||
log.debug("EventHandler: IN_CREATE: %s" % event.pathname)
|
||||
|
||||
def process_IN_DELETE(self, event):
|
||||
log.debug("EventHandler: IN_DELETE: %s" % event.pathname)
|
||||
if self.locks and event.pathname.endswith('.lock'):
|
||||
self.perform_actions(event.pathname[:-5])
|
||||
if self.profile.locks and event.pathname.endswith('.lock'):
|
||||
self.profile.queue.put(event.pathname[:-5])
|
||||
|
||||
def process_IN_MODIFY(self, event):
|
||||
log.debug("EventHandler: IN_MODIFY: %s" % event.pathname)
|
||||
|
||||
def process_IN_MOVED_TO(self, event):
|
||||
log.debug("EventHandler: IN_MOVED_TO: %s" % event.pathname)
|
||||
if not self.locks:
|
||||
self.perform_actions(event.pathname)
|
||||
|
||||
def perform_actions(self, path):
|
||||
for spec, func, args in self.actions:
|
||||
func(path, *args)
|
||||
if not self.profile.locks:
|
||||
self.profile.queue.put(event.pathname)
|
||||
|
||||
|
||||
def get_pid_path():
|
||||
"""
|
||||
Returns the path to the PID file for the file monitor daemon.
|
||||
"""
|
||||
class FileMonitorDaemon(Daemon):
|
||||
|
||||
basename = os.path.basename(sys.argv[0])
|
||||
pid_path = edbob.config.get('%s.filemon' % basename, 'pid_path')
|
||||
if not pid_path:
|
||||
pid_path = '/tmp/%s_filemon.pid' % basename
|
||||
return pid_path
|
||||
|
||||
|
||||
def start_daemon(appname, daemonize=True):
|
||||
"""
|
||||
Starts the file monitor daemon.
|
||||
"""
|
||||
|
||||
pid_path = get_pid_path()
|
||||
if os.path.exists(pid_path):
|
||||
print "File monitor is already running"
|
||||
return
|
||||
def run(self):
|
||||
|
||||
wm = pyinotify.WatchManager()
|
||||
notifier = pyinotify.Notifier(wm)
|
||||
|
||||
monitored = get_monitor_profiles(appname)
|
||||
|
||||
mask = (pyinotify.IN_ACCESS | pyinotify.IN_ATTRIB
|
||||
| pyinotify.IN_CLOSE_WRITE | pyinotify.IN_CREATE
|
||||
| pyinotify.IN_DELETE | pyinotify.IN_MODIFY
|
||||
mask = (pyinotify.IN_ACCESS
|
||||
| pyinotify.IN_ATTRIB
|
||||
| pyinotify.IN_CLOSE_WRITE
|
||||
| pyinotify.IN_CREATE
|
||||
| pyinotify.IN_DELETE
|
||||
| pyinotify.IN_MODIFY
|
||||
| pyinotify.IN_MOVED_TO)
|
||||
for profile in monitored.itervalues():
|
||||
|
||||
monitored = filemon.get_monitor_profiles(self.appname)
|
||||
for key, profile in monitored.iteritems():
|
||||
|
||||
# Create a file queue for the profile.
|
||||
profile.queue = Queue.Queue()
|
||||
|
||||
# Perform setup for each of the watched folders.
|
||||
for path in profile.dirs:
|
||||
wm.add_watch(path, mask, proc_fun=EventHandler(
|
||||
actions=profile.actions, locks=profile.locks))
|
||||
|
||||
if not daemonize:
|
||||
sys.stderr.write("Starting file monitor. (Press Ctrl+C to quit.)\n")
|
||||
notifier.loop(daemonize=daemonize, pid_file=pid_path)
|
||||
# Maybe put all pre-existing files in the queue.
|
||||
if profile.process_existing:
|
||||
filemon.queue_existing(profile, path)
|
||||
|
||||
# Create a watch for the folder.
|
||||
log.debug("start_daemon: profile '%s' watches folder: %s" % (key, path))
|
||||
wm.add_watch(path, mask, proc_fun=EventHandler(profile=profile))
|
||||
|
||||
# Create an action thread for the profile.
|
||||
name = 'actions-%s' % key
|
||||
log.debug("start_daemon: starting action thread: %s" % name)
|
||||
thread = threading.Thread(target=filemon.perform_actions,
|
||||
name=name, args=(profile,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
# Fire up the watchers.
|
||||
notifier.loop()
|
||||
|
||||
|
||||
def stop_daemon():
|
||||
"""
|
||||
Stops the file monitor daemon.
|
||||
"""
|
||||
def get_daemon(appname=None):
|
||||
if appname is None:
|
||||
appname = os.path.basename(sys.argv[0])
|
||||
pid_path = edbob.config.get('%s.filemon' % appname, 'pid_path')
|
||||
if not pid_path:
|
||||
pid_path = '/tmp/%s_filemon.pid' % appname
|
||||
|
||||
pid_path = get_pid_path()
|
||||
if not os.path.exists(pid_path):
|
||||
print "File monitor is not running"
|
||||
return
|
||||
monitor = FileMonitorDaemon(pid_path)
|
||||
monitor.appname = appname
|
||||
return monitor
|
||||
|
||||
f = open(pid_path)
|
||||
pid = f.read().strip()
|
||||
f.close()
|
||||
if not pid.isdigit():
|
||||
log.warning("stop_daemon: Found bogus PID (%s) in file: %s" % (pid, pid_path))
|
||||
return
|
||||
|
||||
os.kill(int(pid), signal.SIGKILL)
|
||||
os.remove(pid_path)
|
||||
def start_daemon(appname):
|
||||
get_daemon(appname).start()
|
||||
|
||||
|
||||
def stop_daemon(appname):
|
||||
get_daemon(appname).stop()
|
||||
|
|
|
@ -33,8 +33,8 @@ import logging
|
|||
import threading
|
||||
|
||||
import edbob
|
||||
from edbob import filemon
|
||||
from edbob.errors import email_exception
|
||||
from edbob.filemon import get_monitor_profiles
|
||||
from edbob.win32 import Service, file_is_free
|
||||
|
||||
if sys.platform == 'win32': # docs should build for everyone
|
||||
|
@ -69,7 +69,7 @@ class FileMonitorService(Service):
|
|||
return False
|
||||
|
||||
# Read monitor profile(s) from config.
|
||||
self.monitored = get_monitor_profiles(self.appname)
|
||||
self.monitored = filemon.get_monitor_profiles(self.appname)
|
||||
|
||||
# Make sure we have something to do.
|
||||
if not self.monitored:
|
||||
|
@ -79,34 +79,36 @@ class FileMonitorService(Service):
|
|||
for key, profile in self.monitored.iteritems():
|
||||
|
||||
# Create a file queue for the profile.
|
||||
queue = Queue.Queue()
|
||||
profile.queue = Queue.Queue()
|
||||
|
||||
# Create a monitor thread for each folder in profile.
|
||||
# Perform setup for each of the watched folders.
|
||||
for i, path in enumerate(profile.dirs, 1):
|
||||
|
||||
# Maybe put all pre-existing files in the queue.
|
||||
if profile.process_existing:
|
||||
filemon.queue_existing(profile, path)
|
||||
|
||||
# Create a monitor thread for the folder.
|
||||
name = 'monitor-%s-%u' % (key, i)
|
||||
log.debug("Initialize: Starting '%s' thread for folder: %s" %
|
||||
(name, path))
|
||||
thread = threading.Thread(
|
||||
target=monitor_files,
|
||||
name=name,
|
||||
args=(queue, path, profile))
|
||||
thread = threading.Thread(target=monitor_files,
|
||||
name=name, args=(profile, path))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
# Create an action thread for the profile.
|
||||
name = 'actions-%s' % key
|
||||
log.debug("Initialize: Starting '%s' thread" % name)
|
||||
thread = threading.Thread(
|
||||
target=perform_actions,
|
||||
name=name,
|
||||
args=(queue, profile))
|
||||
thread = threading.Thread(target=filemon.perform_actions,
|
||||
name=name, args=(profile,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def monitor_files(queue, path, profile):
|
||||
def monitor_files(profile, path):
|
||||
"""
|
||||
Callable target for file monitor threads.
|
||||
"""
|
||||
|
@ -138,40 +140,7 @@ def monitor_files(queue, path, profile):
|
|||
winnt.FILE_ACTION_RENAMED_NEW_NAME):
|
||||
log.debug("monitor_files: Queueing '%s' file: %s" %
|
||||
(profile.key, fpath))
|
||||
queue.put(fpath)
|
||||
|
||||
|
||||
def perform_actions(queue, profile):
|
||||
"""
|
||||
Callable target for action threads.
|
||||
"""
|
||||
|
||||
while True:
|
||||
|
||||
try:
|
||||
path = queue.get_nowait()
|
||||
except Queue.Empty:
|
||||
pass
|
||||
else:
|
||||
|
||||
while not file_is_free(path):
|
||||
win32api.Sleep(0)
|
||||
|
||||
for spec, func, args in profile.actions:
|
||||
|
||||
log.info("perform_actions: Calling function '%s' on file: %s" %
|
||||
(spec, path))
|
||||
|
||||
try:
|
||||
func(path, *args)
|
||||
|
||||
except:
|
||||
log.exception("perform_actions: An exception occurred "
|
||||
"while processing file: %s" % path)
|
||||
email_exception()
|
||||
|
||||
# This file probably shouldn't be processed any further.
|
||||
break
|
||||
profile.queue.put(fpath)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -89,6 +89,7 @@ def init(appname='edbob', *args, **kwargs):
|
|||
shell = kwargs.get('shell', False)
|
||||
for paths in config_paths:
|
||||
config.read(paths, recurse=not shell)
|
||||
if config.getboolean('edbob', 'configure_logging', default=True):
|
||||
config.configure_logging()
|
||||
|
||||
default_modules = 'edbob.time'
|
||||
|
|
|
@ -33,37 +33,38 @@ from sqlalchemy import and_
|
|||
|
||||
# from formalchemy import Field
|
||||
|
||||
import edbob
|
||||
# from edbob.pyramid import filters
|
||||
# from edbob.pyramid import forms
|
||||
# from edbob.pyramid import grids
|
||||
# from edbob.pyramid import Session
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
from edbob.db.extensions.contact.model import (
|
||||
Person, PersonEmailAddress, PersonPhoneNumber)
|
||||
|
||||
|
||||
class PeopleGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = edbob.Person
|
||||
mapped_class = Person
|
||||
config_prefix = 'people'
|
||||
sort = 'first_name'
|
||||
|
||||
def join_map(self):
|
||||
return {
|
||||
'email':
|
||||
lambda q: q.outerjoin(edbob.PersonEmailAddress, and_(
|
||||
edbob.PersonEmailAddress.parent_uuid == edbob.Person.uuid,
|
||||
edbob.PersonEmailAddress.preference == 1)),
|
||||
lambda q: q.outerjoin(PersonEmailAddress, and_(
|
||||
PersonEmailAddress.parent_uuid == Person.uuid,
|
||||
PersonEmailAddress.preference == 1)),
|
||||
'phone':
|
||||
lambda q: q.outerjoin(edbob.PersonPhoneNumber, and_(
|
||||
edbob.PersonPhoneNumber.parent_uuid == edbob.Person.uuid,
|
||||
edbob.PersonPhoneNumber.preference == 1)),
|
||||
lambda q: q.outerjoin(PersonPhoneNumber, and_(
|
||||
PersonPhoneNumber.parent_uuid == Person.uuid,
|
||||
PersonPhoneNumber.preference == 1)),
|
||||
}
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(
|
||||
ilike=['first_name', 'last_name'],
|
||||
email=self.filter_ilike(edbob.PersonEmailAddress.address),
|
||||
phone=self.filter_ilike(edbob.PersonPhoneNumber.number))
|
||||
email=self.filter_ilike(PersonEmailAddress.address),
|
||||
phone=self.filter_ilike(PersonPhoneNumber.number))
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
|
@ -77,8 +78,8 @@ class PeopleGrid(SearchableAlchemyGridView):
|
|||
def sort_map(self):
|
||||
return self.make_sort_map(
|
||||
'first_name', 'last_name',
|
||||
email=self.sorter(edbob.PersonEmailAddress.address),
|
||||
phone=self.sorter(edbob.PersonPhoneNumber.number))
|
||||
email=self.sorter(PersonEmailAddress.address),
|
||||
phone=self.sorter(PersonPhoneNumber.number))
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
|
@ -97,7 +98,7 @@ class PeopleGrid(SearchableAlchemyGridView):
|
|||
|
||||
class PersonCrud(CrudView):
|
||||
|
||||
mapped_class = edbob.Person
|
||||
mapped_class = Person
|
||||
home_route = 'people'
|
||||
|
||||
def fieldset(self, model):
|
||||
|
|
|
@ -32,10 +32,10 @@ import formalchemy
|
|||
from webhelpers.html import tags
|
||||
from webhelpers.html.builder import HTML
|
||||
|
||||
import edbob
|
||||
from edbob.db import auth
|
||||
from edbob.pyramid import Session
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
from edbob.db.extensions.auth.model import Role
|
||||
|
||||
|
||||
default_permissions = [
|
||||
|
@ -68,7 +68,7 @@ default_permissions = [
|
|||
|
||||
class RolesGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = edbob.Role
|
||||
mapped_class = Role
|
||||
config_prefix = 'roles'
|
||||
sort = 'name'
|
||||
|
||||
|
@ -161,7 +161,7 @@ def PermissionsFieldRenderer(permissions, *args, **kwargs):
|
|||
|
||||
class RoleCrud(CrudView):
|
||||
|
||||
mapped_class = edbob.Role
|
||||
mapped_class = Role
|
||||
home_route = 'roles'
|
||||
permissions = default_permissions
|
||||
|
||||
|
|
|
@ -32,28 +32,29 @@ from webhelpers.html.builder import HTML
|
|||
import formalchemy
|
||||
from formalchemy.fields import SelectFieldRenderer
|
||||
|
||||
import edbob
|
||||
from edbob.db import auth
|
||||
from edbob.pyramid import Session
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
from edbob.db.extensions.auth.model import User, Role
|
||||
from edbob.db.extensions.contact.model import Person
|
||||
|
||||
|
||||
class UsersGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = edbob.User
|
||||
mapped_class = User
|
||||
config_prefix = 'users'
|
||||
sort = 'username'
|
||||
|
||||
def join_map(self):
|
||||
return {
|
||||
'person':
|
||||
lambda q: q.outerjoin(edbob.Person),
|
||||
lambda q: q.outerjoin(Person),
|
||||
}
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(
|
||||
ilike=['username'],
|
||||
person=self.filter_ilike(edbob.Person.display_name))
|
||||
person=self.filter_ilike(Person.display_name))
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
|
@ -65,7 +66,7 @@ class UsersGrid(SearchableAlchemyGridView):
|
|||
def sort_map(self):
|
||||
return self.make_sort_map(
|
||||
'username',
|
||||
person=self.sorter(edbob.Person.display_name))
|
||||
person=self.sorter(Person.display_name))
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
|
@ -92,7 +93,7 @@ def RolesFieldRenderer(request):
|
|||
class RolesFieldRenderer(SelectFieldRenderer):
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
roles = Session.query(edbob.Role)
|
||||
roles = Session.query(Role)
|
||||
html = ''
|
||||
for uuid in self.value:
|
||||
role = roles.get(uuid)
|
||||
|
@ -117,15 +118,15 @@ class RolesField(formalchemy.Field):
|
|||
return [x.uuid for x in user.roles]
|
||||
|
||||
def get_options(self):
|
||||
q = Session.query(edbob.Role.name, edbob.Role.uuid)
|
||||
q = q.filter(edbob.Role.uuid != auth.guest_role(Session()).uuid)
|
||||
q = q.order_by(edbob.Role.name)
|
||||
q = Session.query(Role.name, Role.uuid)
|
||||
q = q.filter(Role.uuid != auth.guest_role(Session()).uuid)
|
||||
q = q.order_by(Role.name)
|
||||
return q.all()
|
||||
|
||||
def sync(self):
|
||||
if not self.is_readonly():
|
||||
user = self.model
|
||||
roles = Session.query(edbob.Role)
|
||||
roles = Session.query(Role)
|
||||
data = self.renderer.deserialize()
|
||||
user.roles = [roles.get(x) for x in data]
|
||||
|
||||
|
@ -140,7 +141,7 @@ class _ProtectedPersonRenderer(formalchemy.FieldRenderer):
|
|||
|
||||
|
||||
def ProtectedPersonRenderer(uuid):
|
||||
person = Session.query(edbob.Person).get(uuid)
|
||||
person = Session.query(Person).get(uuid)
|
||||
assert person
|
||||
return type('ProtectedPersonRenderer', (_ProtectedPersonRenderer,),
|
||||
{'person': person})
|
||||
|
@ -187,7 +188,7 @@ class PasswordField(formalchemy.Field):
|
|||
|
||||
class UserCrud(CrudView):
|
||||
|
||||
mapped_class = edbob.User
|
||||
mapped_class = User
|
||||
home_route = 'users'
|
||||
|
||||
def fieldset(self, user):
|
||||
|
@ -213,7 +214,7 @@ class UserCrud(CrudView):
|
|||
del fs.confirm_password
|
||||
|
||||
# if fs.edit and user.person:
|
||||
if isinstance(user, edbob.User) and user.person:
|
||||
if isinstance(user, User) and user.person:
|
||||
fs.person.set(readonly=True,
|
||||
renderer=LinkedPersonRenderer(self.request))
|
||||
|
||||
|
|
19
edbob/tests/__init__.py
Normal file
19
edbob/tests/__init__.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
import unittest
|
||||
|
||||
from pyramid import testing
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
"""
|
||||
Base class for all test suites.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.config = testing.setUp()
|
||||
|
||||
def tearDown(self):
|
||||
testing.tearDown()
|
||||
|
||||
def test_something(self):
|
||||
self.assertTrue(1)
|
|
@ -75,7 +75,7 @@ def init(config):
|
|||
tz = config.get('edbob.time', key)
|
||||
if tz:
|
||||
key = key[5:]
|
||||
log.info("'%s' timezone set to '%s'" % (key, tz))
|
||||
log.debug("'%s' timezone set to '%s'" % (key, tz))
|
||||
set_timezone(tz, key)
|
||||
|
||||
if 'local' not in timezones:
|
||||
|
|
|
@ -38,9 +38,17 @@ if sys.platform == 'win32': # docs should build for everyone
|
|||
import win32file
|
||||
import win32print
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
import winerror
|
||||
|
||||
try:
|
||||
import win32serviceutil
|
||||
except ImportError:
|
||||
# Mock out for testing on Linux.
|
||||
class Object(object):
|
||||
pass
|
||||
win32serviceutil = Object()
|
||||
win32serviceutil.ServiceFramework = Object
|
||||
|
||||
import edbob
|
||||
|
||||
|
||||
|
|
36
fabfile.py
vendored
Normal file
36
fabfile.py
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# edbob -- Pythonic Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of edbob.
|
||||
#
|
||||
# edbob is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# edbob 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 Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with edbob. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
import shutil
|
||||
|
||||
from fabric.api import *
|
||||
|
||||
|
||||
@task
|
||||
def release():
|
||||
"""
|
||||
Release a new version of 'edbob'.
|
||||
"""
|
||||
shutil.rmtree('edbob.egg-info')
|
||||
local('python setup.py sdist --formats=gztar register upload')
|
|
@ -1,2 +1,7 @@
|
|||
[egg_info]
|
||||
tag_build = .dev
|
||||
[nosetests]
|
||||
nocapture = 1
|
||||
cover-package = edbob
|
||||
cover-erase = 1
|
||||
cover-inclusive = 1
|
||||
cover-html = 1
|
||||
cover-html-dir = htmlcov
|
||||
|
|
7
setup.py
7
setup.py
|
@ -73,7 +73,10 @@ requires = [
|
|||
'decorator', # 3.3.2
|
||||
'lockfile', # 0.9.1
|
||||
'progressbar', # 2.3
|
||||
'pytz', # 2012b
|
||||
|
||||
# Hardcode ``pytz`` minimum since apparently it isn't (any longer?) enough
|
||||
# to simply require the library.
|
||||
'pytz>=2013b', # 2013b
|
||||
]
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
|
@ -209,6 +212,8 @@ setup(
|
|||
|
||||
install_requires = requires,
|
||||
extras_require = extras,
|
||||
tests_require = requires + ['nose'],
|
||||
test_suite = 'nose.collector',
|
||||
|
||||
packages = find_packages(),
|
||||
include_package_data = True,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue