Compare commits

...

31 commits

Author SHA1 Message Date
Lance Edgar
a0b35dfadb Remove custom PyPI stuff from Fabric release task. 2014-04-28 14:28:54 -07:00
Lance Edgar
8fabdf8b72 update changelog 2014-04-28 14:27:32 -07:00
Lance Edgar
73978ffeb7 Allow config file to prevent logging configuration from happening. 2014-04-28 14:26:21 -07:00
Lance Edgar
183530afc9 update changelog 2014-01-28 09:13:45 -08:00
Lance Edgar
8a0f300fc0 Add minimum version requirement for pytz. 2013-07-31 22:36:54 -07:00
Lance Edgar
ee9c6622ff Add mock import for testing on Linux. 2013-06-18 22:46:58 -07:00
Lance Edgar
4fe5ad9bf0 Prepare for tests.
This doesn't add any actual tests but it should pave the way for that.  Tests
may be run like so:

.. code-block:: sh

   python setup.py nosetests --with-coverage
2013-06-18 22:19:52 -07:00
Lance Edgar
25611f1e68 Tweaked Fabric `release` command. 2013-05-22 18:09:51 -07:00
Lance Edgar
4ed6b09a24 update changelog 2013-05-21 22:30:03 -07:00
Lance Edgar
3766dbfff1 Updated `repr()` output for model classes. 2013-05-17 12:21:39 -07:00
Lance Edgar
3bfda431fa Changed some logging instances from INFO to DEBUG.
I was just getting tired of the noise.
2013-05-16 07:00:40 -07:00
Lance Edgar
4dc4f3f1ec Removed setup.cfg file.
The `tag_build` setting was not doing us any favors.
2013-05-09 21:47:07 -07:00
Lance Edgar
2e57936ed1 update changelog 2013-04-30 16:16:02 -07:00
Lance Edgar
33c951e7fe Added initial Fabric script. 2013-04-30 16:13:47 -07:00
Lance Edgar
9262b598ac Added --progress argument to command system. 2013-04-30 16:12:20 -07:00
Lance Edgar
6c517a9126 Secured daemon PID files.
They are no longer readable or writeable (whoops) by anyone other than the user
who owns the process.
2013-04-22 08:15:46 -07:00
Lance Edgar
ea7473f18d update changelog 2013-02-17 08:59:47 -08:00
Lance Edgar
896a320da5 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.
2013-02-17 08:46:19 -08:00
Lance Edgar
7ae344403a update changelog 2013-02-12 22:15:18 -08:00
Lance Edgar
884e48970e fix graft() when using dir(module) 2012-11-29 07:19:06 -08:00
Lance Edgar
4bb4143daa tweak checkbox labels in forms.css 2012-11-28 15:48:29 -08:00
Lance Edgar
092e325adc add StrippingFieldRenderer 2012-11-28 09:31:31 -08:00
Lance Edgar
a6488e1089 bump version 2012-11-27 21:38:44 -08:00
Lance Edgar
88dcb1889c update changelog 2012-11-27 21:36:47 -08:00
Lance Edgar
f6bb5b2b77 raise InvalidSpec if no colon 2012-11-27 14:33:23 -08:00
Lance Edgar
64ae8e9136 fix css in progress template 2012-11-27 11:27:31 -08:00
Lance Edgar
d8744f3958 move contact enum to core 2012-11-26 11:15:59 -08:00
Lance Edgar
a1d22df20c add some __unicode__() methods 2012-11-21 10:05:24 -08:00
Lance Edgar
7928461e08 add PHONE_TYPE enum 2012-11-21 10:05:01 -08:00
Lance Edgar
8217b91aa4 add sqlerror_tween 2012-11-14 05:06:31 -08:00
Lance Edgar
c3f58d1b8c bump version 2012-11-13 07:56:54 -08:00
32 changed files with 542 additions and 200 deletions

View file

@ -1,4 +1,76 @@
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
------
- [feature] Added ``StrippingFieldRenderer``.
- [general] Some style tweaks.
- [bug] Fixed ``graft()`` function when called with module reference.
0.1a25
------
- [feature] Added "sqlerror" tween which, in combination with ``pyramid_tm``,
can allow Pyramid apps to gracefully handle database server restarts. Any
transaction which fails due to a disconnection from the server may be
attempted more than once. The assumption is that the second attempt will
succeed, since the underlying connection pool will have been invalidated upon
the first failure.
- [feature] Added ``PHONE_TYPE`` enumeration.
- [general] Added some ``__unicode__()`` methods to database models.
- [bug] Fixed CSS in the ``progress.mako`` template.
- [feature] Improved the ``load_spec()`` function so that it raises an
``InvalidSpec`` exception if the spec doesn't contain exactly one colon.
0.1a24
------

View file

@ -28,6 +28,7 @@
from edbob._version import __version__
from edbob.enum import *
from edbob.core import *
from edbob.time import *
from edbob.files import *

View file

@ -1 +1 @@
__version__ = '0.1a24'
__version__ = '0.1.2'

View file

@ -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

View file

@ -112,7 +112,7 @@ def graft(target, source, names=None):
if hasattr(source, '__all__'):
names = source.__all__
else:
names = dir(source)
names = [x for x in dir(source) if not x.startswith('_')]
elif isinstance(names, basestring):
names = [names]

View file

@ -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)

View file

@ -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()

View file

@ -51,10 +51,11 @@ 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 __str__(self):
return str(self.permission or '')
def __unicode__(self):
return unicode(self.permission or '')
class UserRole(Base):
@ -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,10 +98,10 @@ class Role(Base):
getset_factory=getset_factory)
def __repr__(self):
return "<Role: %s>" % self.name
return "Role(uuid={0})".format(repr(self.uuid))
def __str__(self):
return str(self.name or '')
def __unicode__(self):
return unicode(self.name or '')
class User(Base):
@ -124,17 +125,18 @@ class User(Base):
getset_factory=getset_factory)
def __repr__(self):
return "<User: %s>" % self.username
return "User(uuid={0})".format(repr(self.uuid))
def __str__(self):
return str(self.username or '')
def __unicode__(self):
return unicode(self.username or '')
@property
def display_name(self):
"""
Returns the user's ``person.display_name``, if present, otherwise the
``username``.
Returns :attr:`Person.display_name` if present; otherwise returns
:attr:`username`.
"""
if self.person and self.person.display_name:
return self.person.display_name
return self.username

View file

@ -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 '')

View file

@ -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))

View file

@ -23,7 +23,7 @@
################################################################################
"""
``edbob.db.extensions.contact.enum`` -- Enumerations
``edbob.enum`` -- Enumerations
"""
@ -38,3 +38,14 @@ EMAIL_PREFERENCE = {
EMAIL_PREFERENCE_HTML : "HTML",
EMAIL_PREFERENCE_MOBILE : "Mobile",
}
PHONE_TYPE_HOME = 'home'
PHONE_TYPE_MOBILE = 'mobile'
PHONE_TYPE_OTHER = 'other'
PHONE_TYPE = {
PHONE_TYPE_HOME : "Home",
PHONE_TYPE_MOBILE : "Mobile",
PHONE_TYPE_OTHER : "Other",
}

View file

@ -73,6 +73,12 @@ class LoadSpecError(Exception):
return None
class InvalidSpec(LoadSpecError):
def specifics(self):
return "invalid spec"
class ModuleMissingAttribute(LoadSpecError):
"""
Raised during :func:`edbob.load_spec()` when the module imported okay but

View file

@ -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)

View file

@ -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()

View file

@ -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__':

View file

@ -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'

View file

@ -82,6 +82,9 @@ def load_spec(spec):
necessary.
"""
if spec.count(':') != 1:
raise exceptions.InvalidSpec(spec)
module_path, obj = spec.split(':')
module = import_module_path(module_path)
try:

View file

@ -50,7 +50,8 @@ from edbob.pyramid.forms.formalchemy.renderers import *
__all__ = ['ChildGridField', 'PropertyField', 'EnumFieldRenderer',
'PrettyDateTimeFieldRenderer', 'AutocompleteFieldRenderer',
'FieldSet', 'make_fieldset', 'required', 'pretty_datetime',
'AssociationProxyField', 'YesNoFieldRenderer']
'AssociationProxyField', 'StrippingFieldRenderer',
'YesNoFieldRenderer']
class TemplateEngine(formalchemy.templates.TemplateEngine):

View file

@ -32,7 +32,7 @@ from pyramid.renderers import render
__all__ = ['AutocompleteFieldRenderer', 'EnumFieldRenderer',
'YesNoFieldRenderer']
'StrippingFieldRenderer', 'YesNoFieldRenderer']
def AutocompleteFieldRenderer(service_url, field_value=None, field_display=None, width='300px'):
@ -83,6 +83,17 @@ def EnumFieldRenderer(enum):
return Renderer
class StrippingFieldRenderer(formalchemy.TextFieldRenderer):
"""
Standard text field renderer, which strips whitespace from either end of
the input value on deserialization.
"""
def deserialize(self):
value = super(StrippingFieldRenderer, self).deserialize()
return value.strip()
class YesNoFieldRenderer(formalchemy.fields.CheckBoxFieldRenderer):
def render_readonly(self, **kwargs):

View file

@ -78,6 +78,15 @@ div.field-wrapper div.field textarea {
width: 320px;
}
label input[type=checkbox] {
margin-right: 8px;
}
/******************************
* Buttons
******************************/
div.buttons {
clear: both;
margin-top: 10px;

View file

@ -5,7 +5,8 @@
<title>Working...</title>
${h.javascript_link(request.static_url('edbob.pyramid:static/js/jquery.js'))}
${h.javascript_link(request.static_url('edbob.pyramid:static/js/edbob.js'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/edbob.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/base.css'))}
${h.stylesheet_link(request.static_url('edbob.pyramid:static/css/layout.css'))}
<style type="text/css">
#container {

59
edbob/pyramid/tweens.py Normal file
View file

@ -0,0 +1,59 @@
#!/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/>.
#
################################################################################
"""
``edbob.pyramid.tweens`` -- Tween Factories
"""
import sqlalchemy.exc
from transaction.interfaces import TransientError
def sqlerror_tween_factory(handler, registry):
"""
Produces a tween which will convert ``sqlalchemy.exc.OperationalError``
instances (caused by database server restart) into a retryable
``transaction.interfaces.TransientError`` instance, so that a second
attempt may be made to connect to the database before really giving up.
.. note::
This tween alone is not enough to cause the transaction to be retried;
it only marks the error as being *retryable*. If you wish more than one
attempt to be made, you must define the ``tm.attempts`` setting within
your Pyramid app configuration. See `Retrying
<http://docs.pylonsproject.org/projects/pyramid_tm/en/latest/#retrying>`_
for more information.
"""
def sqlerror_tween(request):
try:
response = handler(request)
except sqlalchemy.exc.OperationalError, error:
if error.connection_invalidated:
raise TransientError(str(error))
raise
return response
return sqlerror_tween

View file

@ -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):

View file

@ -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

View file

@ -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))

View file

@ -7,10 +7,8 @@
import os.path
from pyramid.config import Configurator
from pyramid.authentication import SessionAuthenticationPolicy
import edbob
from edbob.pyramid.auth import EdbobAuthorizationPolicy
def main(global_config, **settings):
@ -27,24 +25,18 @@ def main(global_config, **settings):
# * Raise an exception if a setting is missing or invalid.
# * Convert values from strings to their intended type.
settings['mako.directories'] = [
settings.setdefault('mako.directories', [
'{{package}}.pyramid:templates',
'edbob.pyramid:templates',
]
])
# Make two attempts when "retryable" errors happen during transactions.
settings.setdefault('tm.attempts', 2)
config = Configurator(settings=settings)
# Configure edbob
edbob.init('{{package}}', os.path.abspath(settings['edbob.config']))
# Configure session
config.include('pyramid_beaker')
# Configure auth
config.set_authentication_policy(SessionAuthenticationPolicy())
config.set_authorization_policy(EdbobAuthorizationPolicy())
# Include "core" stuff provided by edbob.
config.include('edbob.pyramid')
# Additional config is defined elsewhere within {{project}}. This includes
@ -53,4 +45,8 @@ def main(global_config, **settings):
config.include('{{package}}.pyramid.subscribers')
config.include('{{package}}.pyramid.views')
# Consider PostgreSQL server restart errors to be "retryable."
config.add_tween('edbob.pyramid.tweens.sqlerror_tween_factory',
under='pyramid_tm.tm_tween_factory')
return config.make_wsgi_app()

19
edbob/tests/__init__.py Normal file
View 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)

View file

@ -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:

View file

@ -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
View 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')

View file

@ -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

View file

@ -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,