diff --git a/COPYING.txt b/COPYING.txt
index 94a9ed0..dba13ed 100644
--- a/COPYING.txt
+++ b/COPYING.txt
@@ -1,5 +1,5 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
@@ -7,17 +7,15 @@
Preamble
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
+our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
+software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -72,7 +60,7 @@ modification follow.
0. Definitions.
- "This License" refers to version 3 of the GNU General Public License.
+ "This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
- 13. Use with the GNU Affero General Public License.
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
+under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
+Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
+GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
+versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
+ 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.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU General Public License
+ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
+For more information on this, and how to apply and follow the GNU AGPL, see
.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
diff --git a/README.txt b/README.txt
index ea72f6d..ffaabfe 100644
--- a/README.txt
+++ b/README.txt
@@ -2,13 +2,15 @@
edbob
=====
-edbob is a Pythonic software framework, released under the GPL.
+edbob is a Pythonic software framework, released under the GNU Affero General
+Public License.
It aims to be "environment-neutral" in that it can assist with development for
console, web, or GUI applications. Pay only for what you eat; however all of
its functionality combined may be considered a "full stack" of sorts.
-For more information, please see http://edbob.org/.
+For more information, please see `edbob.org `_ or send email
+to `Lance Edgar `_.
Installation
@@ -16,10 +18,6 @@ Installation
Install the software with::
- $ easy_install edbob
-
-or::
-
$ pip install edbob
diff --git a/edbob/__init__.py b/edbob/__init__.py
index d312716..39a1e43 100644
--- a/edbob/__init__.py
+++ b/edbob/__init__.py
@@ -2,23 +2,23 @@
# -*- coding: utf-8 -*-
################################################################################
#
-# edbob -- Pythonic software framework
-# Copyright © 2010,2011,2012 Lance Edgar
+# 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 General Public License as published by the Free Software
-# Foundation, either version 3 of the License, or (at your option) any later
-# version.
+# 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 General Public License for more
-# details.
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
#
-# You should have received a copy of the GNU General Public License along with
-# edbob. If not, see .
+# You should have received a copy of the GNU Affero General Public License
+# along with edbob. If not, see .
#
################################################################################
@@ -29,6 +29,11 @@
from edbob._version import __version__
from edbob.core import *
+from edbob.times import *
from edbob.files import *
from edbob.modules import *
-from edbob.times import *
+from edbob.configuration import *
+from edbob.initialization import *
+
+
+inited = False
diff --git a/edbob/commands.py b/edbob/commands.py
new file mode 100644
index 0000000..ff7ff87
--- /dev/null
+++ b/edbob/commands.py
@@ -0,0 +1,299 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.commands`` -- Console Commands
+"""
+
+import sys
+import argparse
+import subprocess
+import logging
+
+import edbob
+from edbob.util import requires_impl
+
+
+class ArgumentParser(argparse.ArgumentParser):
+ """
+ Customized version of ``argparse.ArgumentParser``, which overrides some of
+ the argument parsing logic. This is necessary for the application's
+ primary command (:class:`Command` class); but is not used with
+ :class:`Subcommand` derivatives.
+ """
+
+ def parse_args(self, args=None, namespace=None):
+ args, argv = self.parse_known_args(args, namespace)
+ args.argv = argv
+ return args
+
+
+class Command(edbob.Object):
+ """
+ The primary command for the application.
+
+ You should subclass this within your own application if you wish to
+ leverage the command system provided by edbob.
+ """
+
+ name = 'edbob'
+ version = edbob.__version__
+ description = "Pythonic Software Framework"
+
+ long_description = """
+edbob is a Pythonic software framework.
+
+Copyright (c) 2010-2012 Lance Edgar
+
+This program comes with ABSOLUTELY NO WARRANTY. This is free software,
+and you are welcome to redistribute it under certain conditions.
+See the file COPYING.txt for more information.
+"""
+
+ def __init__(self, **kwargs):
+ edbob.Object.__init__(self, **kwargs)
+ self.subcommands = edbob.entry_point_map('%s.commands' % self.name)
+
+ def __str__(self):
+ return str(self.name)
+
+ def iter_subcommands(self):
+ """
+ Generator which yields associated :class:`Subcommand` classes, sorted
+ by :attr:`Subcommand.name`.
+ """
+
+ for name in sorted(self.subcommands):
+ yield self.subcommands[name]
+
+ def print_help(self):
+ """
+ Prints help text for the primary command, including a list of available
+ subcommands.
+ """
+
+ print """%(description)s
+
+Usage: %(name)s [options] [command-options]
+
+Options:
+ -v, --verbose Increase logging level to INFO
+ -V, --version Display program version and exit
+
+Subcommands:""" % self
+
+ for cmd in self.iter_subcommands():
+ print " %-12s %s" % (cmd.name, cmd.description)
+
+ def run(self, *args):
+ """
+ Parses ``args`` and executes the appropriate subcommand action
+ accordingly (or displays help text).
+ """
+
+ parser = ArgumentParser(
+ prog=self.name,
+ description=self.description,
+ add_help=False,
+ )
+
+ parser.add_argument('-v', '--verbose', action='store_true', dest='verbose')
+ parser.add_argument('-V', '--version', action='version',
+ version="%%(prog)s %s" % self.version)
+ parser.add_argument('subcommand', nargs='*')
+
+ args = parser.parse_args(list(args))
+ if not args or not args.subcommand:
+ self.print_help()
+ return
+
+ cmd = args.subcommand.pop(0)
+ if cmd == 'help':
+ if len(args.subcommand) != 1:
+ self.print_help()
+ return
+ cmd = args.subcommand[0]
+ if cmd not in self.subcommands:
+ self.print_help()
+ return
+ cmd = self.subcommands[cmd](parent=self)
+ cmd.parser.print_help()
+ return
+ elif cmd not in self.subcommands:
+ self.print_help()
+ return
+
+ if args.verbose:
+ logging.getLogger().setLevel(logging.INFO)
+
+ cmd = self.subcommands[cmd](parent=self)
+ cmd._run(*args.argv)
+
+
+class Subcommand(edbob.Object):
+ """
+ Base class for application "subcommands." You'll want to derive from this
+ class as needed to implement most of your application's command logic.
+ """
+
+ def __init__(self, **kwargs):
+ edbob.Object.__init__(self, **kwargs)
+ self.parser = argparse.ArgumentParser(
+ prog='%s %s' % (self.parent, self.name),
+ description=self.description,
+ )
+ self.add_parser_args(self.parser)
+
+ def __repr__(self):
+ return "" % self.name
+
+ def add_parser_args(self, parser):
+ """
+ If your subcommand accepts optional (or positional) arguments, you
+ should override this and add the possible arguments directly to parser
+ (which is a :class:`argparse.ArgumentParser` instance) via its
+ :meth:`add_argument()` method.
+ """
+ pass
+
+ @property
+ @requires_impl(is_property=True)
+ def description(self):
+ """
+ The description for the subcommand. This must be provided within your
+ subclass, as a simple class attribute.
+ """
+ pass
+
+ @property
+ @requires_impl(is_property=True)
+ def name(self):
+ """
+ The name of the subcommand. This must be provided within your
+ subclass, as a simple class attribute.
+
+ .. note::
+ The subcommand name should ideally be limited to 12 characters in
+ order to preserve formatting consistency when displaying help text.
+ """
+ pass
+
+ def _run(self, *args):
+ args = self.parser.parse_args(list(args))
+ return self.run(args)
+
+ @requires_impl()
+ def run(self, args):
+ """
+ Runs the subcommand. You must override this method within your
+ subclass. ``args`` will be a :class:`argparse.Namespace` object
+ containing all parsed arguments found on the original command line
+ executed by the user.
+ """
+ pass
+
+
+class ShellCommand(Subcommand):
+ """
+ Launches a Python shell (of your choice) with ``edbob`` pre-loaded; called
+ as ``edbob shell``.
+
+ You can configure the shell within ``edbob.conf`` (otherwise ``python`` is
+ assumed)::
+
+ [edbob]
+ shell.python = ipython
+ """
+
+ name = 'shell'
+ description = "Launch Python shell with edbob pre-loaded"
+
+ def run(self, args):
+ code = ['import edbob']
+ if edbob.inited:
+ code.append("edbob.init('edbob', %s, shell=True)" %
+ edbob.config.paths_attempted)
+ code.append('from edbob import *')
+ code = '; '.join(code)
+ print "edbob v%s launching Python shell...\n" % edbob.__version__
+ python = 'python'
+ if edbob.inited:
+ python = edbob.config.get('edbob', 'shell.python', default=python)
+ proc = subprocess.Popen([python, '-i', '-c', code])
+ while True:
+ try:
+ proc.wait()
+ except KeyboardInterrupt:
+ pass
+ else:
+ break
+
+
+class UuidCommand(Subcommand):
+ """
+ Command for generating an UUID; called as ``edbob uuid``.
+
+ If the ``--gui`` option is specified, this command launches a small
+ wxPython application for generating UUIDs; otherwise a single UUID will be
+ printed to the console.
+ """
+
+ name = 'uuid'
+ description = "Generate an universally-unique identifier"
+
+ def add_parser_args(self, parser):
+ parser.add_argument('--gui', action='store_true',
+ help="Display graphical interface")
+
+ def run(self, args):
+ if args.gui:
+ from edbob.wx.GenerateUuid import main
+ main()
+ else:
+ print edbob.get_uuid()
+
+
+def main(*args):
+ """
+ The primary entry point for the edbob command system.
+
+ .. note::
+ This entry point is really for ``edbob`` proper. Your application
+ should provide its own command entry point which leverages *your*
+ :class:`Command` subclass instead of using this entry point (which uses
+ :class:`Command` directly).
+
+ There's not much involved in doing so; see the source code for
+ implementation details.
+ """
+
+ if args:
+ args = list(args)
+ else:
+ args = sys.argv[1:]
+
+ edbob.init()
+
+ cmd = Command()
+ cmd.run(*args)
diff --git a/edbob/configuration.py b/edbob/configuration.py
new file mode 100644
index 0000000..c3ad593
--- /dev/null
+++ b/edbob/configuration.py
@@ -0,0 +1,355 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.configuration`` -- Configuration
+"""
+
+import os
+import os.path
+import sys
+import ConfigParser
+import logging
+import logging.config
+
+import edbob
+from edbob import exceptions
+
+
+__all__ = ['AppConfigParser']
+
+log = logging.getLogger(__name__)
+
+
+class AppConfigParser(ConfigParser.SafeConfigParser):
+ """
+ Subclass of ``ConfigParser.SafeConfigParser``, with some conveniences
+ added.
+ """
+
+ def __init__(self, appname, *args, **kwargs):
+ ConfigParser.SafeConfigParser.__init__(self, *args, **kwargs)
+ self.appname = appname
+ self.paths_attempted = []
+ self.paths_loaded = []
+
+ def clear(self):
+ """
+ Completely clears the contents of the config instance.
+ """
+
+ for section in self.sections():
+ self.remove_section(section)
+ del self.paths_attempted[:]
+ del self.paths_loaded[:]
+
+ def configure_logging(self):
+ """
+ Saves the current (possibly cascaded) configuration to a temporary
+ file, and passes that to ``logging.config.fileConfig()``.
+ """
+
+ if self.getboolean(self.appname, 'basic_logging', default=False):
+ edbob.basic_logging(self.appname)
+ path = edbob.temp_path(suffix='.conf')
+ self.save(path)
+ try:
+ logging.config.fileConfig(path)
+ except ConfigParser.NoSectionError:
+ pass
+ os.remove(path)
+
+ def get(self, section, option, raw=False, vars=None, default=None):
+ """
+ Overridden version of ``ConfigParser.SafeConfigParser.get()``; this one
+ adds the ``default`` keyword parameter and will return it instead of
+ raising an error when the option doesn't exist.
+ """
+
+ if self.has_option(section, option):
+ return ConfigParser.SafeConfigParser.get(self, section, option, raw, vars)
+ return default
+
+ def getboolean(self, section, option, default=None):
+ """
+ Overriddes base class method to allow for a default.
+ """
+
+ try:
+ val = ConfigParser.SafeConfigParser.getboolean(self, section, option)
+ except AttributeError:
+ return default
+ return val
+
+ def get_dict(self, section):
+ """
+ Convenience method which returns a dictionary of options contained
+ within the given section.
+ """
+
+ d = {}
+ for opt in self.options(section):
+ d[opt] = self.get(section, opt)
+ return d
+
+ def options(self, section):
+ """
+ Overridden version of ``ConfigParser.SafeConfigParser.options()``.
+ This one doesn't raise an error if ``section`` doesn't exist, but
+ instead returns an empty list.
+ """
+
+ if not self.has_section(section):
+ return []
+ return ConfigParser.SafeConfigParser.options(self, section)
+
+ def read(self, paths, recurse=True):
+ r"""
+ .. highlight:: ini
+
+ Overrides the ``RawConfigParser`` method by implementing the following
+ logic:
+
+ Prior to actually reading the contents of the file(s) specified by
+ ``paths`` into the current config instance, a recursive algorithm will
+ inspect the config found in the file(s) to see if additional config
+ file(s) are to be included. All config files, whether specified
+ directly by ``paths`` or indirectly by way of primary configuration,
+ are finally read into the current config instance in the proper order
+ so that cascading works as expected.
+
+ If you pass ``recurse=False`` to this method then none of the magical
+ inclusion logic will happen at all.
+
+ Note that when a config file indicates that another file(s) is to be
+ included, the referenced file will be read into this config instance
+ *before* the original (primary) file is read into it. A convenient
+ setup then could be to maintain a "site-wide" config file, shared on
+ the network, including something like this::
+
+ # //file-server/share/edbob/site.conf
+ #
+ # This file contains settings relevant to all machines on the
+ # network. Mail and logging configuration at least would be good
+ # candidates for inclusion here.
+
+ [edbob.mail]
+ smtp.server = mail.example.com
+ # smtp.username = user
+ # smtp.password = pass
+ sender.default = noreply@example.com
+ recipients.default = ['tech-support@example.com']
+
+ [loggers]
+ keys = root, edbob
+
+ # ...etc. The bulk of logging configuration would go here.
+
+ Then a config file local to a particular machine could look something
+ like this::
+
+ # C:\ProgramData\edbob\edbob.conf
+ #
+ # This file contains settings specific to the local machine.
+
+ [edbob]
+ include_config = [r'\\file-server\share\edbob\site.conf']
+
+ # Add any local app config here, e.g. connection details for a
+ # database, etc.
+
+ # All logging config is inherited from the site-wide file, except
+ # we'll override the output file location so that it remains local.
+ # And maybe we need the level bumped up while we troubleshoot
+ # something.
+
+ [handler_file]
+ args = (r'C:\ProgramData\edbob\edbob.log', 'a')
+
+ [logger_edbob]
+ level = DEBUG
+
+ There is no right way to do this of course; the need should drive the
+ method. Since recursion is used, there is also no real limit to how
+ you go about it. A config file specific to a particular app on a
+ particular machine can further include a config file specific to the
+ local user on that machine, which in turn can include a file specific
+ to the local machine generally, which could then include one or more
+ site-wide files, etc. Or the "most specific" (initially read; primary)
+ config file could indicate which other files to include for every level
+ of that, in which case recursion would be less necessary (though still
+ technically used).
+ """
+
+ if isinstance(paths, basestring):
+ paths = [paths]
+ for path in paths:
+ self.read_path(path, recurse=recurse)
+ return self.paths_loaded
+
+ def read_path(self, path, recurse=True):
+ """
+ .. highlight:: ini
+
+ Reads a "single" config file into the instance. If ``recurse`` is ``True``,
+ *and* the config file references other "parent" config file(s), then the
+ parent(s) are read also in recursive fashion.
+
+ "Parent" config file paths may be specified in this way::
+
+ [edbob]
+ include_config = [
+ r'\\file-server\share\edbob\site.conf',
+ r'C:\ProgramData\edbob\special-stuff.conf',
+ ]
+
+ See :meth:`read()` for more information.
+ """
+
+ self.paths_attempted.append(path)
+ log.debug("Reading config file: %s" % path)
+ if not os.path.exists(path):
+ log.debug("File doesn't exist")
+ return
+ config = ConfigParser.SafeConfigParser()
+ if not config.read(path):
+ log.debug("Read failed")
+ return
+ include = None
+ if recurse:
+ if (config.has_section(self.appname) and
+ config.has_option(self.appname, 'include_config')):
+ include = config.get(self.appname, 'include_config')
+ if include:
+ log.debug("Including config: %s" % include)
+ for p in eval(include):
+ self.read_path(p)
+ ConfigParser.SafeConfigParser.read(self, path)
+ if include:
+ self.remove_option(self.appname, 'include_config')
+ self.paths_loaded.append(path)
+ log.info("Read config file: %s" % path)
+
+ def require(self, section, option, msg=None):
+ """
+ Convenience method which will raise an exception if the given option
+ does not exist. ``msg`` can be used to override (some of) the error
+ text.
+ """
+
+ value = self.get(section, option)
+ if value:
+ return value
+ raise exceptions.ConfigError(section, option, msg)
+
+ def save(self, filename, create_dir=True):
+ """
+ Saves the current config contents to a file. Optionally can create the
+ parent folder(s) as necessary.
+ """
+
+ config_folder = os.path.dirname(filename)
+ if create_dir and not os.path.exists(config_folder):
+ os.makedirs(config_folder)
+ config_file = open(filename, 'w')
+ self.write(config_file)
+ config_file.close()
+
+ def set(self, section, option, value):
+ """
+ Overrides ``ConfigParser.SafeConfigParser.set()`` so that ``section``
+ is created if it doesn't already exist, instead of raising an error.
+ """
+
+ if not self.has_section(section):
+ self.add_section(section)
+ ConfigParser.SafeConfigParser.set(self, section, option, value)
+
+
+def default_system_paths(appname):
+ r"""
+ Returns a list of default system-level config file paths for the given
+ ``appname``, according to ``sys.platform``.
+
+ For example, assuming an app name of ``'rattail'``, the following would be
+ returned:
+
+ ``win32``:
+ * ``\rattail.conf``
+ * ``\rattail\rattail.conf``
+
+ Any other platform:
+ * ``/etc/rattail.conf``
+ * ``/etc/rattail/rattail.conf``
+ * ``/usr/local/etc/rattail.conf``
+ * ``/usr/local/etc/rattail/rattail.conf``
+ """
+
+ if sys.platform == 'win32':
+ from win32com.shell import shell, shellcon
+ return [
+ os.path.join(shell.SHGetSpecialFolderPath(
+ 0, shellcon.CSIDL_COMMON_APPDATA), '%s.conf' % appname),
+ os.path.join(shell.SHGetSpecialFolderPath(
+ 0, shellcon.CSIDL_COMMON_APPDATA), appname, '%s.conf' % appname),
+ ]
+
+ return [
+ '/etc/%s.conf' % appname,
+ '/etc/%s/%s.conf' % (appname, appname),
+ '/usr/local/etc/%s.conf' % appname,
+ '/usr/local/etc/%s/%s.conf' % (appname, appname),
+ ]
+
+
+def default_user_paths(appname):
+ r"""
+ Returns a list of default user-level config file paths for the given
+ ``appname``, according to ``sys.platform``.
+
+ For example, assuming an app name of ``'rattail'``, the following would be
+ returned:
+
+ ``win32``:
+ * ``\rattail.conf``
+ * ``\rattail\rattail.conf``
+
+ Any other platform:
+ * ``~/.rattail.conf``
+ * ``~/.rattail/rattail.conf``
+ """
+
+ if sys.platform == 'win32':
+ from win32com.shell import shell, shellcon
+ return [
+ os.path.join(shell.SHGetSpecialFolderPath(
+ 0, shellcon.CSIDL_APPDATA), '%s.conf' % appname),
+ os.path.join(shell.SHGetSpecialFolderPath(
+ 0, shellcon.CSIDL_APPDATA), appname, '%s.conf' % appname),
+ ]
+
+ return [
+ os.path.expanduser('~/.%s.conf' % appname),
+ os.path.expanduser('~/.%s/%s.conf' % (appname, appname)),
+ ]
diff --git a/edbob/console.py b/edbob/console.py
new file mode 100644
index 0000000..ed6896e
--- /dev/null
+++ b/edbob/console.py
@@ -0,0 +1,50 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.console`` -- Console-Specific Stuff
+"""
+
+import sys
+import progressbar
+
+import edbob
+
+
+class Progress(edbob.Object):
+ """
+ Provides a console-based progress bar.
+ """
+
+ def __init__(self, message, maximum):
+ print >> sys.stderr, "\n%s...(%u total)" % (message, maximum)
+ widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ', progressbar.ETA()]
+ self.progress = progressbar.ProgressBar(maxval=maximum, widgets=widgets).start()
+
+ def update(self, value):
+ self.progress.update(value)
+ return True
+
+ def destroy(self):
+ print >> sys.stderr, ''
diff --git a/edbob/core.py b/edbob/core.py
index ec917a2..adb2a39 100644
--- a/edbob/core.py
+++ b/edbob/core.py
@@ -2,23 +2,23 @@
# -*- coding: utf-8 -*-
################################################################################
#
-# edbob -- Pythonic software framework
-# Copyright © 2010,2011,2012 Lance Edgar
+# 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 General Public License as published by the Free Software
-# Foundation, either version 3 of the License, or (at your option) any later
-# version.
+# 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 General Public License for more
-# details.
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
#
-# You should have received a copy of the GNU General Public License along with
-# edbob. If not, see .
+# You should have received a copy of the GNU Affero General Public License
+# along with edbob. If not, see .
#
################################################################################
@@ -29,15 +29,16 @@
import logging
import uuid
+from pkg_resources import iter_entry_points
-__all__ = ['Object', 'basic_logging', 'get_uuid']
+__all__ = ['Object', 'basic_logging', 'entry_point_map', 'get_uuid', 'graft']
class Object(object):
"""
- Generic base class which provides a common ancestor, and some constructor
- convenience.
+ Generic base class which provides a common ancestor, and some other
+ conveniences.
"""
def __init__(self, **kwargs):
@@ -49,18 +50,43 @@ class Object(object):
for key in kwargs:
setattr(self, key, kwargs[key])
+ def __getitem__(self, key):
+ """
+ Allows dict-like access to the object's attributes.
+ """
-def basic_logging():
+ if hasattr(self, key):
+ return getattr(self, key)
+
+
+def basic_logging(appname):
"""
- Does some basic configuration on the ``edbob`` logger. This only enables
- console output at this point; it is assumed that if you intend to configure
- logging that you will be using a proper config file and calling
- :func:`edbob.init()`.
+ Does some basic configuration on the logger qualified by ``appname``.
+
+ .. note::
+ This only enables console output at this point; it is assumed that if
+ you intend to "truly" configure logging that you will be using a proper
+ config file and calling :func:`edbob.init()`.
"""
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(name)s: %(levelname)s: %(message)s'))
- logging.getLogger('edbob').addHandler(handler)
+ logging.getLogger(appname).addHandler(handler)
+
+
+def entry_point_map(key):
+ """
+ Convenience function to retrieve a dictionary of entry points, keyed by
+ name.
+
+ ``key`` must be the "section name" for the entry points you're after, e.g.
+ ``'edbob.commands'``.
+ """
+
+ epmap = {}
+ for ep in iter_entry_points(key):
+ epmap[ep.name] = ep.load()
+ return epmap
def get_uuid():
@@ -70,3 +96,35 @@ def get_uuid():
"""
return uuid.uuid1().hex
+
+
+def graft(target, source, names=None):
+ """
+ Adds names to the ``target`` namespace, copying each from ``source``.
+
+ If ``names`` is provided, it can be a string if adding only one thing;
+ otherwise it should be a list of strings. If it is not provided, then
+ everything from ``source`` will be grafted.
+
+ .. note::
+ If "everything" is to be grafted (i.e. ``names is None``), then
+ ``source.__all__`` will be consulted if available. If it is not, then
+ ``dir(source)`` will be used instead.
+ """
+
+ if names is None:
+ if hasattr(source, '__all__'):
+ names = source.__all__
+ else:
+ names = dir(source)
+ elif isinstance(names, basestring):
+ names = [names]
+
+ for name in names:
+ if hasattr(source, name):
+ setattr(target, name, getattr(source, name))
+ else:
+ setattr(target, name, source.get(name))
+ if not hasattr(target, '__all__'):
+ target.__all__ = []
+ target.__all__.append(name)
diff --git a/edbob/db/__init__.py b/edbob/db/__init__.py
new file mode 100644
index 0000000..b498266
--- /dev/null
+++ b/edbob/db/__init__.py
@@ -0,0 +1,134 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.db`` -- Database Framework
+"""
+
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+
+import edbob
+
+
+Session = sessionmaker()
+
+
+def init():
+ """
+ Called whenever ``'edbob.db'`` is configured to be auto-initialized.
+
+ This function is responsible for establishing the primary database engine
+ (a ``sqlalchemy.Engine`` instance, read from config), and extending the
+ root ``edbob`` namespace with the ORM classes (``Person``, ``User``, etc.),
+ as well as a few other things, e.g. ``engine``, ``Session`` and
+ ``metadata``.
+
+ In addition to this, if a connection to the primary database can be
+ obtained, it will be consulted to see which extensions are active within
+ it. If any are found, edbob's ORM will be extended in-place accordingly.
+ """
+
+ config = edbob.config.get_dict('edbob.db')
+ engine = engine_from_config(config)
+ edbob.graft(edbob, locals(), 'engine')
+
+ Session.configure(bind=engine)
+ edbob.graft(edbob, globals(), 'Session')
+
+ from edbob.db.model import get_metadata
+ metadata = get_metadata(bind=engine)
+ edbob.graft(edbob, locals(), 'metadata')
+
+ from edbob.db.mappers import make_mappers
+ make_mappers(metadata)
+
+ from edbob.db.ext import extend_framework
+ extend_framework()
+
+ # Note that we extend the framework before we graft the 'classes' module
+ # contents, since extensions may graft things to that module.
+ import edbob.db.classes as classes
+ edbob.graft(edbob, classes)
+
+ # Same goes for the enum module.
+ import edbob.db.enum as enum
+ edbob.graft(edbob, enum)
+
+ # Add settings functions.
+ edbob.graft(edbob, globals(), ('get_setting', 'save_setting'))
+
+
+def get_setting(name, session=None):
+ """
+ Returns a setting from the database.
+ """
+
+ _session = session
+ if not session:
+ session = Session()
+ setting = session.query(edbob.Setting).get(name)
+ if setting:
+ setting = setting.value
+ if not _session:
+ session.close()
+ return setting
+
+
+def save_setting(name, value, session=None):
+ """
+ Saves a setting to the database.
+ """
+
+ _session = session
+ if not session:
+ session = Session()
+ setting = session.query(edbob.Setting).get(name)
+ if not setting:
+ setting = edbob.Setting(name=name)
+ session.add(setting)
+ setting.value = value
+ if not _session:
+ session.commit()
+ session.close()
+
+
+def needs_session(func):
+ """
+ Decorator which adds helpful session handling.
+ """
+
+ def wrapped(*args, **kwargs):
+ session = kwargs.get('session')
+ _session = session
+ if not session:
+ session = Session()
+ kwargs['session'] = session
+ res = func(session, *args, **kwargs)
+ if not _session:
+ session.commit()
+ session.close()
+ return res
+
+ return wrapped
diff --git a/edbob/db/auth.py b/edbob/db/auth.py
new file mode 100644
index 0000000..6669ed9
--- /dev/null
+++ b/edbob/db/auth.py
@@ -0,0 +1,72 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.db.auth`` -- Authentication & Authorization
+"""
+
+from sqlalchemy.orm import object_session
+
+from edbob.db.classes import Role, User
+
+
+def get_administrator(session):
+ """
+ Returns a :class:`edbob.Role` instance representing the "Administrator"
+ role, attached to the given ``session``.
+ """
+
+ uuid = 'd937fa8a965611dfa0dd001143047286'
+ admin = session.query(Role).get(uuid)
+ if admin:
+ return admin
+ admin = Role(uuid=uuid, name='Administrator')
+ session.add(admin)
+ return admin
+
+
+def has_permission(obj, perm):
+ """
+ Checks the given ``obj`` (which may be either a :class:`edbob.User`` or
+ :class:`edbob.Role` instance), and returns a boolean indicating whether or
+ not the object is allowed the given permission. ``perm`` should be a
+ fully-qualified permission name, e.g. ``'users.create'``.
+ """
+
+ if isinstance(obj, User):
+ roles = obj.roles
+ elif isinstance(obj, Role):
+ roles = [obj]
+ else:
+ raise TypeError, "You must pass either a User or Role for 'obj'; got: %s" % repr(obj)
+ session = object_session(obj)
+ assert session
+ admin = get_administrator(session)
+ for role in roles:
+ if role is admin:
+ return True
+ for permission in role.permissions:
+ if permission == perm:
+ return True
+ return False
diff --git a/edbob/db/classes.py b/edbob/db/classes.py
new file mode 100644
index 0000000..656a6e5
--- /dev/null
+++ b/edbob/db/classes.py
@@ -0,0 +1,163 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.db.classes`` -- Data Class Definitions
+"""
+
+from sqlalchemy.ext.associationproxy import association_proxy
+
+import edbob
+from edbob.sqlalchemy import getset_factory
+
+
+__all__ = ['Permission', 'Person', 'Role', 'Setting', 'User', 'UserRole']
+
+
+class PersonDerivative(edbob.Object):
+ """
+ Base class for classes which must derive certain functionality from the
+ :class:`Person` class, e.g. :class:`User`.
+ """
+
+ display_name = association_proxy('person', 'display_name',
+ creator=lambda x: Person(display_name=x),
+ getset_factory=getset_factory)
+
+ first_name = association_proxy('person', 'first_name',
+ creator=lambda x: Person(first_name=x),
+ getset_factory=getset_factory)
+
+ last_name = association_proxy('person', 'last_name',
+ creator=lambda x: Person(last_name=x),
+ getset_factory=getset_factory)
+
+
+class ActiveExtension(edbob.Object):
+ """
+ Represents an extension which has been activated within a database.
+ """
+
+ def __repr__(self):
+ return "" % self.name
+
+
+class Permission(edbob.Object):
+ """
+ Represents the fact that a particular :class:`Role` is allowed to do a
+ certain thing.
+ """
+
+ def __repr__(self):
+ return "" % (self.role, self.permission)
+
+
+class Person(edbob.Object):
+ """
+ Represents a real, living and breathing person. (Or, at least was
+ previously living and breathing, in the case of the deceased.)
+ """
+
+ def __repr__(self):
+ return "" % self.display_name
+
+ def __str__(self):
+ return str(self.display_name or '')
+
+ @property
+ def customer(self):
+ """
+ Returns the first :class:`Customer` instance in
+ :attr:`Person.customers`, or ``None`` if that list is empty.
+
+ .. note::
+ As of this writing, :attr:`Person.customers` is an
+ arbitrarily-ordered list, so the only real certainty you have when
+ using :attr:`Person.customer` is when the :class:`Person` instance
+ is associated with exactly one (or zero) :class:`Customer`
+ instances.
+ """
+
+ if self.customers:
+ return self.customers[0]
+ return None
+
+
+class Role(edbob.Object):
+ """
+ Represents a role within the organization; used to manage permissions.
+ """
+
+ permissions = association_proxy('_permissions', 'permission',
+ creator=lambda x: Permission(permission=x),
+ getset_factory=getset_factory)
+
+ users = association_proxy('_users', 'user',
+ creator=lambda x: UserRole(user=x),
+ getset_factory=getset_factory)
+
+ def __repr__(self):
+ return "" % self.name
+
+ def __str__(self):
+ return str(self.name or '')
+
+
+class Setting(edbob.Object):
+ """
+ Represents a setting stored within the database.
+ """
+
+ def __repr__(self):
+ return "" % self.name
+
+
+class User(PersonDerivative):
+ """
+ Represents a user of the system. This may or may not correspond to a real
+ person, e.g. for data import jobs and the like.
+ """
+
+ employee = association_proxy('person', 'employee',
+ creator=lambda x: Person(employee=x),
+ getset_factory=getset_factory)
+
+ roles = association_proxy('_roles', 'role',
+ creator=lambda x: UserRole(role=x),
+ getset_factory=getset_factory)
+
+ def __repr__(self):
+ return "" % self.username
+
+ def __str__(self):
+ return str(self.username or '')
+
+
+class UserRole(edbob.Object):
+ """
+ Represents the association between a :class:`User` and a :class:`Role`.
+ """
+
+ def __repr__(self):
+ return "" % (self.user, self.role)
diff --git a/edbob/db/mappers.py b/edbob/db/mappers.py
new file mode 100644
index 0000000..2691d61
--- /dev/null
+++ b/edbob/db/mappers.py
@@ -0,0 +1,129 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.db.mappers`` -- Object Relational Mappings
+"""
+
+from sqlalchemy.orm import mapper, relationship
+
+import edbob.db.classes as c
+
+
+def make_mappers(metadata):
+ """
+ This function glues together the schema definition found in the ``models``
+ module with the data class definitions found in the ``classes`` module.
+ ``metadata`` should be a ``sqlalchemy.MetaData`` instance.
+
+ It is meant to be called only once, by :func:`edbob.init()`.
+ """
+
+ t = metadata.tables
+
+
+ # ActiveExtension
+ mapper(
+ c.ActiveExtension, t['active_extensions'],
+ )
+
+
+ # Permission
+ mapper(
+ c.Permission, t['permissions'],
+ )
+
+
+ # Person
+ mapper(
+ c.Person, t['people'],
+ properties=dict(
+
+ customers=relationship(
+ c.Customer,
+ backref='person',
+ ),
+
+ employee=relationship(
+ c.Employee,
+ back_populates='person',
+ uselist=False,
+ ),
+
+ user=relationship(
+ c.User,
+ back_populates='person',
+ uselist=False,
+ ),
+ ),
+ )
+
+
+ # Role
+ mapper(
+ c.Role, t['roles'],
+ properties=dict(
+
+ _permissions=relationship(
+ c.Permission,
+ backref='role',
+ ),
+
+ _users=relationship(
+ c.UserRole,
+ backref='role',
+ ),
+ ),
+ )
+
+
+ # Setting
+ mapper(
+ c.Setting, t['settings'],
+ )
+
+
+ # User
+ mapper(
+ c.User, t['users'],
+ properties=dict(
+
+ person=relationship(
+ c.Person,
+ back_populates='user',
+ ),
+
+ _roles=relationship(
+ c.UserRole,
+ backref='user',
+ cascade='save-update,merge,delete',
+ ),
+ ),
+ )
+
+
+ # UserRole
+ mapper(
+ c.UserRole, t['users_roles'],
+ )
diff --git a/edbob/db/model.py b/edbob/db/model.py
new file mode 100644
index 0000000..92c4942
--- /dev/null
+++ b/edbob/db/model.py
@@ -0,0 +1,109 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.db.model`` -- Core Schema Definition
+"""
+
+from sqlalchemy import *
+
+from edbob import get_uuid
+
+
+def get_metadata(*args, **kwargs):
+ """
+ Returns the core ``edbob`` schema definition.
+
+ Note that when :func:`edbob.init()` is called, the ``sqlalchemy.MetaData``
+ instance which is returned from this function will henceforth be available
+ as ``edbob.metadata``. However, ``edbob.init()`` may extend
+ ``edbob.metadata`` as well, depending on which extensions are activated
+ within the primary database.
+
+ This function then serves two purposes: First, it provides the core
+ metadata instance. Secondly, it allows edbob to always know what its core
+ schema looks like, as opposed to what's held in the current
+ ``edbob.metadata`` instance, which may have been extended locally. (The
+ latter use is necessary in order for edbob to properly manage its
+ extensions.)
+
+ All arguments (positional and keyword) are passed directly to the
+ ``sqlalchemy.MetaData()`` constructor.
+ """
+
+ metadata = MetaData(*args, **kwargs)
+
+ active_extensions = Table(
+ 'active_extensions', metadata,
+ Column('name', String(50), primary_key=True),
+ )
+
+ def get_person_display_name(context):
+ first_name = context.current_parameters['first_name']
+ last_name = context.current_parameters['last_name']
+ if not (first_name or last_name):
+ return None
+ return '%(first_name)s %(last_name)s' % locals()
+
+ people = Table(
+ 'people', metadata,
+ Column('uuid', String(32), primary_key=True, default=get_uuid),
+ Column('first_name', String(50)),
+ Column('last_name', String(50)),
+ Column('display_name', String(100), default=get_person_display_name),
+ )
+
+ permissions = Table(
+ 'permissions', metadata,
+ Column('role_uuid', String(32), ForeignKey('roles.uuid'), primary_key=True),
+ Column('permission', String(50), primary_key=True),
+ )
+
+ roles = Table(
+ 'roles', metadata,
+ Column('uuid', String(32), primary_key=True, default=get_uuid),
+ Column('name', String(25), nullable=False, unique=True),
+ )
+
+ settings = Table(
+ 'settings', metadata,
+ Column('name', String(255), primary_key=True),
+ Column('value', Text),
+ )
+
+ users = Table(
+ 'users', metadata,
+ Column('uuid', String(32), primary_key=True, default=get_uuid),
+ Column('username', String(25), nullable=False, unique=True),
+ Column('person_uuid', String(32), ForeignKey('people.uuid')),
+ )
+
+ users_roles = Table(
+ 'users_roles', metadata,
+ Column('uuid', String(32), primary_key=True, default=get_uuid),
+ Column('user_uuid', String(32), ForeignKey('users.uuid')),
+ Column('role_uuid', String(32), ForeignKey('roles.uuid')),
+ )
+
+ return metadata
diff --git a/edbob/db/perms.py b/edbob/db/perms.py
new file mode 100644
index 0000000..379af7b
--- /dev/null
+++ b/edbob/db/perms.py
@@ -0,0 +1,84 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.db.perms`` -- Roles & Permissions
+"""
+
+from sqlalchemy.orm import object_session
+
+from edbob.db.classes import Role, User, Permission
+
+
+def get_administrator(session):
+ """
+ Returns the "Administrator" :class:`rattail.db.classes.Role` instance,
+ attached to the given ``session``.
+ """
+
+ uuid = 'd937fa8a965611dfa0dd001143047286'
+ admin = session.query(Role).get(uuid)
+ if admin:
+ return admin
+ admin = Role(uuid=uuid, name='Administrator')
+ session.add(admin)
+ return admin
+
+
+# def has_permission(object_, permission, session=None):
+# '''
+# Checks the given ``object_`` (which may be either a :class:`rattail.v1.User` or
+# a :class:`rattail.v1.Role`) and returns a boolean indicating whether or not the
+# object is allowed the given permission. ``permission`` may be either a
+# :class:`rattail.v1.Permission` instance, or the fully-qualified name of one.
+
+# If ``object_`` is ``None``, the permission check is made against the special
+# "(Anybody)" role.
+# '''
+
+def has_permission(obj, perm):
+ """
+ Checks the given ``obj`` (which may be either a
+ :class:`rattail.db.classes.User`` or :class:`rattail.db.classes.Role`
+ instance), and returns a boolean indicating whether or not the object is
+ allowed the given permission. ``perm`` should be a fully-qualified
+ permission name, e.g. ``'employees.admin'``.
+ """
+
+ if isinstance(obj, User):
+ roles = obj.roles
+ elif isinstance(obj, Role):
+ roles = [obj]
+ else:
+ raise TypeError, "You must pass either a User or Role for 'obj'; got: %s" % repr(obj)
+ session = object_session(obj)
+ assert session
+ admin = get_administrator(session)
+ for role in roles:
+ if role is admin:
+ return True
+ for permission in role.permissions:
+ if permission == perm:
+ return True
+ return False
diff --git a/edbob/exceptions.py b/edbob/exceptions.py
index 4950092..16ce3c5 100644
--- a/edbob/exceptions.py
+++ b/edbob/exceptions.py
@@ -2,23 +2,23 @@
# -*- coding: utf-8 -*-
################################################################################
#
-# edbob -- Pythonic software framework
-# Copyright © 2010,2011,2012 Lance Edgar
+# 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 General Public License as published by the Free Software
-# Foundation, either version 3 of the License, or (at your option) any later
-# version.
+# 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 General Public License for more
-# details.
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
#
-# You should have received a copy of the GNU General Public License along with
-# edbob. If not, see .
+# You should have received a copy of the GNU Affero General Public License
+# along with edbob. If not, see .
#
################################################################################
@@ -27,6 +27,21 @@
"""
+class ConfigError(Exception):
+ """
+ Raised when configuration is missing or otherwise invalid.
+ """
+
+ def __init__(self, section, option, msg=None):
+ self.section = section
+ self.option = option
+ self.msg = msg or "Missing or invalid config"
+
+ def __str__(self):
+ return "%s; please set '%s' in the [%s] section of your config file" % (
+ self.msg, self.option, self.section)
+
+
class LoadSpecError(Exception):
"""
Raised when something obvious goes wrong with :func:`edbob.load_spec()`.
@@ -55,3 +70,27 @@ class ModuleMissingAttribute(LoadSpecError):
def specifics(self):
mod, attr = self.spec.split(':')
return "module '%s' was loaded but '%s' attribute not found" % (mod, attr)
+
+
+class RecipientsNotFound(Exception):
+ """
+ Raised when no recipients could be found in config.
+ """
+
+ def __init__(self, key):
+ self.key = key
+
+ def __str__(self):
+ return "No recipients configured (set 'recipients.%s' in [edbob.mail])" % self.key
+
+
+class SenderNotFound(Exception):
+ """
+ Raised when no sender could be found in config.
+ """
+
+ def __init__(self, key):
+ self.key = key
+
+ def __str__(self):
+ return "No sender configured (set 'sender.%s' in [edbob.mail])" % self.key
diff --git a/edbob/files.py b/edbob/files.py
index 600f08d..886fcec 100644
--- a/edbob/files.py
+++ b/edbob/files.py
@@ -2,23 +2,23 @@
# -*- coding: utf-8 -*-
################################################################################
#
-# edbob -- Pythonic software framework
-# Copyright © 2010,2011,2012 Lance Edgar
+# 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 General Public License as published by the Free Software
-# Foundation, either version 3 of the License, or (at your option) any later
-# version.
+# 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 General Public License for more
-# details.
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
#
-# You should have received a copy of the GNU General Public License along with
-# edbob. If not, see .
+# You should have received a copy of the GNU Affero General Public License
+# along with edbob. If not, see .
#
################################################################################
@@ -28,10 +28,31 @@
import os
+import os.path
+import shutil
import tempfile
-__all__ = ['count_lines', 'temp_path']
+__all__ = ['change_newlines', 'count_lines', 'temp_path']
+
+
+def change_newlines(path, newline):
+ """
+ Rewrites the file at ``path``, changing its newline character(s) to that of
+ ``newline``.
+ """
+
+ root, ext = os.path.splitext(path)
+ temp_path = temp_path(suffix='.' + ext)
+ infile = open(path, 'rUb')
+ outfile = open(temp_path, 'wb')
+ for line in infile:
+ line = line.rstrip('\r\n')
+ outfile.write(line + newline)
+ infile.close()
+ outfile.close()
+ os.remove(path)
+ shutil.move(temp_path, path)
def count_lines(path):
diff --git a/edbob/initialization.py b/edbob/initialization.py
new file mode 100644
index 0000000..b513733
--- /dev/null
+++ b/edbob/initialization.py
@@ -0,0 +1,101 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.initialization`` -- Initialization Framework
+"""
+
+import os
+# import locale
+import logging
+
+from edbob.configuration import AppConfigParser
+from edbob.configuration import default_system_paths, default_user_paths
+from edbob.core import graft
+from edbob.times import set_timezone
+
+
+__all__ = ['init']
+
+log = logging.getLogger(__name__)
+
+
+def init(appname='edbob', *args, **kwargs):
+ """
+ Initializes the edbob framework, typically by first reading some config
+ file(s) to determine which interfaces to engage. This must normally be
+ called prior to doing anything really useful, as it is responsible for
+ extending the live API in-place.
+
+ The meaning of ``args`` is as follows:
+
+ If ``args`` is empty, the ``EDBOB_CONFIG`` environment variable is first
+ consulted. If it is nonempty, then its value is split according to
+ ``os.pathsep`` and the resulting sequence is passed to
+ ``edbob.config.read()``.
+
+ If both ``args`` and ``EDBOB_CONFIG`` are empty, the "standard" locations
+ are assumed, and the results of calling both
+ :func:`edbob.configuration.default_system_paths()` and
+ :func:`edbob.configuration.default_user_paths()` are passed on to
+ ``edbob.config.read()``.
+
+ Any other values in ``args`` will be passed directly to
+ ``edbob.config.read()`` and so will be interpreted there. Basically they
+ are assumed to be either strings, or sequences of strings, which represent
+ paths to various config files, each being read in the order in which it
+ appears within ``args``. (Configuration is thereby cascaded such that the
+ file read last will override those before it.)
+ """
+
+ config = AppConfigParser(appname)
+
+ if args:
+ config_paths = list(args)
+ elif os.environ.get('EDBOB_CONFIG'):
+ config_paths = os.environ['EDBOB_CONFIG'].split(os.pathsep)
+ else:
+ config_paths = default_system_paths(appname) + default_user_paths(appname)
+
+ shell = bool(kwargs.get('shell'))
+ for paths in config_paths:
+ config.read(paths, recurse=not shell)
+ config.configure_logging()
+
+ # loc = config.get('edbob', 'locale')
+ # if loc:
+ # locale.setlocale(locale.LC_ALL, loc)
+ # log.info("Set locale to '%s'" % loc)
+
+ tz = config.get('edbob', 'timezone')
+ if tz:
+ set_timezone(tz)
+ log.info("Set timezone to '%s'" % tz)
+ else:
+ log.warning("No timezone configured; falling back to US/Central")
+ set_timezone('US/Central')
+
+ import edbob
+ graft(edbob, locals(), 'config')
+ edbob.inited = True
diff --git a/edbob/lib/__init__.py b/edbob/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/edbob/lib/pretty.py b/edbob/lib/pretty.py
new file mode 100644
index 0000000..602caf2
--- /dev/null
+++ b/edbob/lib/pretty.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+pretty
+
+Formats dates, numbers, etc. in a pretty, human readable format.
+"""
+__author__ = "S Anand (sanand@s-anand.net)"
+__copyright__ = "Copyright 2010, S Anand"
+__license__ = "WTFPL"
+
+
+# Note that some modifications exist between this file and the original as
+# downloaded from http://pypi.python.org/pypi/py-pretty
+
+
+from datetime import datetime
+
+import edbob
+
+
+def _df(seconds, denominator=1, text='', past=True):
+ if past: return str((seconds + denominator/2)/ denominator) + text + ' ago'
+ else: return 'in ' + str((seconds + denominator/2)/ denominator) + text
+
+
+def date(time=False, asdays=False, short=False):
+ '''Returns a pretty formatted date.
+ Inputs:
+ time is a datetime object or an int timestamp
+ asdays is True if you only want to measure days, not seconds
+ short is True if you want "1d ago", "2d ago", etc. False if you want
+ '''
+
+ now = edbob.utc_time()
+ if type(time) is int: time = datetime.fromtimestamp(time)
+ elif not time: time = now
+
+ if time > now: past, diff = False, time - now
+ else: past, diff = True, now - time
+ seconds = diff.seconds
+ days = diff.days
+
+ if short:
+ if days == 0 and not asdays:
+ if seconds < 10: return 'now'
+ elif seconds < 60: return _df(seconds, 1, 's', past)
+ elif seconds < 3600: return _df(seconds, 60, 'm', past)
+ else: return _df(seconds, 3600, 'h', past)
+ else:
+ if days == 0: return 'today'
+ elif days == 1: return past and 'yest' or 'tom'
+ elif days < 7: return _df(days, 1, 'd', past)
+ elif days < 31: return _df(days, 7, 'w', past)
+ elif days < 365: return _df(days, 30, 'mo', past)
+ else: return _df(days, 365, 'y', past)
+ else:
+ if days == 0 and not asdays:
+ if seconds < 10: return 'now'
+ elif seconds < 60: return _df(seconds, 1, ' seconds', past)
+ elif seconds < 120: return past and 'a minute ago' or 'in a minute'
+ elif seconds < 3600: return _df(seconds, 60, ' minutes', past)
+ elif seconds < 7200: return past and 'an hour ago' or'in an hour'
+ else: return _df(seconds, 3600, ' hours', past)
+ else:
+ if days == 0: return 'today'
+ elif days == 1: return past and 'yesterday' or'tomorrow'
+ elif days == 2: return past and 'day before' or 'day after'
+ elif days < 7: return _df(days, 1, ' days', past)
+ elif days < 14: return past and 'last week' or 'next week'
+ elif days < 31: return _df(days, 7, ' weeks', past)
+ elif days < 61: return past and 'last month' or 'next month'
+ elif days < 365: return _df(days, 30, ' months', past)
+ elif days < 730: return past and 'last year' or 'next year'
+ else: return _df(days, 365, ' years', past)
diff --git a/edbob/mail.py b/edbob/mail.py
new file mode 100644
index 0000000..981bc96
--- /dev/null
+++ b/edbob/mail.py
@@ -0,0 +1,139 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.mail`` -- Email Framework
+"""
+
+import logging
+import smtplib
+from email.message import Message
+
+import edbob
+from edbob import exceptions
+
+
+log = logging.getLogger(__name__)
+
+
+def sendmail(sender, recipients, subject, body, content_type='text/plain'):
+ """
+ Sends an email message from ``sender`` to each address in ``recipients``
+ (which should be a sequence), with subject ``subject`` and body ``body``.
+
+ If sending an HTML message instead of plain text, be sure to set
+ ``content_type`` to ``'text/html'``.
+ """
+ message = Message()
+ message.set_type(content_type)
+ message['From'] = sender
+ for recipient in recipients:
+ message['To'] = recipient
+ message['Subject'] = subject
+ message.set_payload(body)
+
+ server = edbob.config.get('edbob.mail', 'smtp.server',
+ default='localhost')
+ username = edbob.config.get('edbob.mail', 'smtp.username')
+ password = edbob.config.get('edbob.mail', 'smtp.password')
+
+ log.debug("sendmail: connecting to server: %s" % server)
+ session = smtplib.SMTP(server)
+ if username and password:
+ res = session.login(username, password)
+ log.debug("sendmail: login result is: %s" % str(res))
+ res = session.sendmail(message['From'], message.get_all('To'), message.as_string())
+ log.debug("sendmail: sendmail result is: %s" % res)
+ session.quit()
+
+
+def get_sender(key):
+ sender = edbob.config.get('edbob.mail', 'sender.'+key)
+ if sender:
+ return sender
+ sender = edbob.config.get('edbob.mail', 'sender.default')
+ if sender:
+ return sender
+ raise exceptions.SenderNotFound(key)
+
+
+def get_recipients(key):
+ recips = edbob.config.get('edbob.mail', 'recipients.'+key)
+ if recips:
+ return eval(recips)
+ recips = edbob.config.get('edbob.mail', 'recipients.default')
+ if recips:
+ return eval(recips)
+ raise exceptions.RecipientsNotFound(key)
+
+
+def get_subject(key):
+ subject = edbob.config.get('edbob.mail', 'subject.'+key)
+ if subject:
+ return subject
+ subject = edbob.config.get('edbob.mail', 'subject.default')
+ if subject:
+ return subject
+ return "[edbob]"
+
+
+def sendmail_with_config(key, body, subject=None, **kwargs):
+ """
+ .. highlight:: ini
+
+ Sends mail using sender/recipient/subject values found in config, according
+ to ``key``. Probably the easiest way to explain would be to show an example::
+
+ [edbob.mail]
+ smtp.server = localhost
+ sender.default = Lance Edgar
+ subject.default = A Nice Shrubbery, Not Too Expensive
+
+ recipients.nightly_reports = ['Lance Edgar ']
+
+ subject.tragic_errors = The World Is Nearing The End!!
+ recipients.tragic_errors = ['Lance Edgar ']
+
+ Anything not configured explicitly will fall back to defaults where
+ possible. Note however that a sender and recipients (default or otherwise)
+ *must* be found or else an exception will be raised.
+
+ The above does not include a default recipient list, but it would work the same
+ as the subject and sender as far as the key goes. To send these mails then::
+
+ from edbob.mail import sendmail_with_config
+
+ # just a report
+ sendmail_with_config('nightly_reports', open('report.txt').read())
+
+ # you might want to sit down for this one...
+ sendmail_with_config('tragic_errors', open('OMGWTFBBQ.txt').read())
+
+ If you do not provide ``subject`` to this function, and there is no
+ ``subject.default`` setting found in config, a default of "[edbob]" will
+ be used.
+ """
+ if subject is None:
+ subject = get_subject(key)
+ return sendmail(get_sender(key), get_recipients(key), subject, body, **kwargs)
diff --git a/edbob/modules.py b/edbob/modules.py
index 8de5cc7..e39c756 100644
--- a/edbob/modules.py
+++ b/edbob/modules.py
@@ -2,23 +2,23 @@
# -*- coding: utf-8 -*-
################################################################################
#
-# edbob -- Pythonic software framework
-# Copyright © 2010,2011,2012 Lance Edgar
+# 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 General Public License as published by the Free Software
-# Foundation, either version 3 of the License, or (at your option) any later
-# version.
+# 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 General Public License for more
-# details.
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
#
-# You should have received a copy of the GNU General Public License along with
-# edbob. If not, see .
+# You should have received a copy of the GNU Affero General Public License
+# along with edbob. If not, see .
#
################################################################################
@@ -35,6 +35,18 @@ from edbob import exceptions
__all__ = ['load_spec']
+def import_module_path(module_path):
+ """
+ Imports and returns an arbitrary Python module, given its "dotted" path
+ (i.e. not its file path).
+ """
+
+ if module_path in sys.modules:
+ return sys.modules[module_path]
+ module = __import__(module_path)
+ return last_module(module, module_path)
+
+
def last_module(module, module_path):
"""
Returns the "last" module represented by ``module_path``, by walking
@@ -57,18 +69,6 @@ def last_module(module, module_path):
return last_module(child, '.'.join(parts))
-def import_module_path(module_path):
- """
- Imports and returns an arbitrary Python module, given its "dotted" path
- (i.e. not its file path).
- """
-
- if module_path in sys.modules:
- return sys.modules[module_path]
- module = __import__(module_path)
- return last_module(module, module_path)
-
-
def load_spec(spec):
"""
.. highlight:: none
diff --git a/edbob/pyramid/__init__.py b/edbob/pyramid/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/edbob/pyramid/forms/__init__.py b/edbob/pyramid/forms/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/edbob/pyramid/forms/formalchemy.py b/edbob/pyramid/forms/formalchemy.py
new file mode 100644
index 0000000..1530389
--- /dev/null
+++ b/edbob/pyramid/forms/formalchemy.py
@@ -0,0 +1,28 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.pyramid.forms.formalchemy`` -- FormAlchemy Interface
+"""
+
diff --git a/edbob/pyramid/handlers/__init__.py b/edbob/pyramid/handlers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/edbob/pyramid/handlers/base.py b/edbob/pyramid/handlers/base.py
new file mode 100644
index 0000000..a62f9e4
--- /dev/null
+++ b/edbob/pyramid/handlers/base.py
@@ -0,0 +1,291 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.pyramid.handlers.base`` -- Base Handlers
+"""
+
+from pyramid.renderers import render_to_response
+from pyramid.httpexceptions import HTTPException, HTTPFound, HTTPOk, HTTPUnauthorized
+
+import sqlahelper
+
+# import rattail.pyramid.forms.util as util
+from rattail.db.perms import has_permission
+from rattail.pyramid.forms.formalchemy import Grid
+
+
+class needs_perm(object):
+ """
+ Decorator to be used for handler methods which should restrict access based
+ on the current user's permissions.
+ """
+
+ def __init__(self, permission, **kwargs):
+ self.permission = permission
+ self.kwargs = kwargs
+
+ def __call__(self, fn):
+ permission = self.permission
+ kw = self.kwargs
+ def wrapped(self):
+ if not self.request.current_user:
+ self.request.session['referrer'] = self.request.url_generator.current()
+ self.request.session.flash("You must be logged in to do that.", 'error')
+ return HTTPFound(location=self.request.route_url('login'))
+ if not has_permission(self.request.current_user, permission):
+ self.request.session.flash("You do not have permission to do that.", 'error')
+ home = kw.get('redirect', self.request.route_url('home'))
+ return HTTPFound(location=home)
+ return fn(self)
+ return wrapped
+
+
+def needs_user(fn):
+ """
+ Decorator for handler methods which require simply that a user be currently
+ logged in.
+ """
+
+ def wrapped(self):
+ if not self.request.current_user:
+ self.request.session['referrer'] = self.request.url_generator.current()
+ self.request.session.flash("You must be logged in to do that.", 'error')
+ return HTTPFound(location=self.request.route_url('login'))
+ return fn(self)
+ return wrapped
+
+
+class Handler(object):
+
+ def __init__(self, request):
+ self.request = request
+ self.Session = sqlahelper.get_session()
+
+ # def json_response(self, data={}):
+ # response = render_to_response('json', data, request=self.request)
+ # response.headers['Content-Type'] = 'application/json'
+ # return response
+
+
+class CrudHandler(Handler):
+ # """
+ # This handler provides all the goodies typically associated with general
+ # CRUD functionality, e.g. search filters and grids.
+ # """
+
+ def crud(self, cls, fieldset_factory, home=None, delete=None, post_sync=None, pre_render=None):
+ """
+ Adds a common CRUD mechanism for objects.
+
+ ``cls`` should be a SQLAlchemy-mapped class, presumably deriving from
+ :class:`rattail.Object`.
+
+ ``fieldset_factory`` must be a callable which accepts the fieldset's
+ "model" as its only positional argument.
+
+ ``home`` will be used as the redirect location once a form is fully
+ validated and data saved. If you do not speficy this parameter, the
+ user will be redirected to be the CRUD page for the new object (e.g. so
+ an object may be created before certain properties may be edited).
+
+ ``delete`` may either be a string containing a URL to which the user
+ should be redirected after the object has been deleted, or else a
+ callback which will be executed *instead of* the normal algorithm
+ (which is merely to delete the object via the Session).
+
+ ``post_sync`` may be a callback which will be executed immediately
+ after ``FieldSet.sync()`` is called, i.e. after validation as well.
+
+ ``pre_render`` may be a callback which will be executed after any POST
+ processing has occured, but just before rendering.
+ """
+
+ uuid = self.request.params.get('uuid')
+ obj = self.Session.query(cls).get(uuid) if uuid else cls
+ assert obj
+
+ if self.request.params.get('delete'):
+ if delete:
+ if isinstance(delete, basestring):
+ self.Session.delete(obj)
+ return HTTPFound(location=delete)
+ res = delete(obj)
+ if res:
+ return res
+ else:
+ self.Session.delete(obj)
+ if not home:
+ raise ValueError("Must specify 'home' or 'delete' url "
+ "in call to CrudHandler.crud()")
+ return HTTPFound(location=home)
+
+ fs = fieldset_factory(obj)
+
+ # if not fs.readonly and self.request.params.get('fieldset'):
+ # fs.rebind(data=self.request.params)
+ # if fs.validate():
+ # fs.sync()
+ # if post_sync:
+ # res = post_sync(fs)
+ # if isinstance(res, HTTPFound):
+ # return res
+ # if self.request.params.get('partial'):
+ # self.Session.flush()
+ # return self.json_success(uuid=fs.model.uuid)
+ # return HTTPFound(location=self.request.route_url(objects, action='index'))
+
+ if not fs.readonly and self.request.POST:
+ # print self.request.POST
+ fs.rebind(data=self.request.params)
+ if fs.validate():
+ fs.sync()
+ if post_sync:
+ res = post_sync(fs)
+ if res:
+ return res
+ if self.request.params.get('partial'):
+ self.Session.flush()
+ return self.json_success(uuid=fs.model.uuid)
+
+ if not home:
+ self.Session.flush()
+ home = self.request.url_generator.current() + '?uuid=' + fs.model.uuid
+ self.request.session.flash("%s \"%s\" has been %s." % (
+ fs.crud_title, fs.get_display_text(),
+ 'updated' if fs.edit else 'created'))
+ return HTTPFound(location=home)
+
+ data = {'fieldset': fs, 'crud': True}
+
+ if pre_render:
+ res = pre_render(fs)
+ if res:
+ if isinstance(res, HTTPException):
+ return res
+ data.update(res)
+
+ # data = {'fieldset':fs}
+ # if self.request.params.get('partial'):
+ # return render_to_response('/%s/crud_partial.mako' % objects,
+ # data, request=self.request)
+ # return data
+
+ return data
+
+ def grid(self, *args, **kwargs):
+ """
+ Convenience function which returns a grid. The only functionality this
+ method adds is the ``session`` parameter.
+ """
+
+ return Grid(session=self.Session(), *args, **kwargs)
+
+ # def get_grid(self, name, grid, query, search=None, url=None, **defaults):
+ # """
+ # Convenience function for obtaining the configuration for a grid,
+ # and then obtaining the grid itself.
+
+ # ``name`` is essentially the config key, e.g. ``'products.lookup'``, and
+ # in fact is expected to take that precise form (where the first part is
+ # considered the handler name and the second part the action name).
+
+ # ``grid`` must be a callable with a signature of ``grid(query,
+ # config)``, and ``query`` will be passed directly to the ``grid``
+ # callable. ``search`` will be used to inform the grid of the search in
+ # effect, if any. ``defaults`` will be used to customize the grid config.
+ # """
+
+ # if not url:
+ # handler, action = name.split('.')
+ # url = self.request.route_url(handler, action=action)
+ # config = util.get_grid_config(name, self.request, search,
+ # url=url, **defaults)
+ # return grid(query, config)
+
+ # def get_search_form(self, name, labels={}, **defaults):
+ # """
+ # Convenience function for obtaining the configuration for a search form,
+ # and then obtaining the form itself.
+
+ # ``name`` is essentially the config key, e.g. ``'products.lookup'``.
+ # The ``labels`` dictionary can be used to override the default labels
+ # displayed for the various search fields. The ``defaults`` dictionary
+ # is used to customize the search config.
+ # """
+
+ # config = util.get_search_config(name, self.request,
+ # self.filter_map(), **defaults)
+ # form = util.get_search_form(config, **labels)
+ # return form
+
+ # def object_crud(self, cls, objects=None, post_sync=None):
+ # """
+ # This method is a desperate attempt to encapsulate shared CRUD logic
+ # which is useful across all editable data objects.
+
+ # ``objects``, if provided, should be the plural name for the class as
+ # used in internal naming, e.g. ``'products'``. A default will be used
+ # if you do not provide this value.
+
+ # ``post_sync``, if provided, should be a callable which accepts a
+ # ``formalchemy.Fieldset`` instance as its only argument. It will be
+ # called immediately after the fieldset is synced.
+ # """
+
+ # if not objects:
+ # objects = cls.__name__.lower() + 's'
+
+ # uuid = self.request.params.get('uuid')
+ # obj = self.Session.query(cls).get(uuid) if uuid else cls
+ # assert obj
+
+ # fs = self.fieldset(obj)
+
+ # if not fs.readonly and self.request.params.get('fieldset'):
+ # fs.rebind(data=self.request.params)
+ # if fs.validate():
+ # fs.sync()
+ # if post_sync:
+ # res = post_sync(fs)
+ # if isinstance(res, HTTPFound):
+ # return res
+ # if self.request.params.get('partial'):
+ # self.Session.flush()
+ # return self.json_success(uuid=fs.model.uuid)
+ # return HTTPFound(location=self.request.route_url(objects, action='index'))
+
+ # data = {'fieldset':fs}
+ # if self.request.params.get('partial'):
+ # return render_to_response('/%s/crud_partial.mako' % objects,
+ # data, request=self.request)
+ # return data
+
+ # def render_grid(self, grid, search=None, **kwargs):
+ # """
+ # Convenience function to render a standard grid. Really just calls
+ # :func:`dtail.forms.util.render_grid()`.
+ # """
+
+ # return util.render_grid(self.request, grid, search, **kwargs)
diff --git a/edbob/pyramid/handlers/util.py b/edbob/pyramid/handlers/util.py
new file mode 100644
index 0000000..0530947
--- /dev/null
+++ b/edbob/pyramid/handlers/util.py
@@ -0,0 +1,72 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.pyramid.handlers.util`` -- Handler Utilities
+"""
+
+from pyramid.httpexceptions import HTTPFound
+
+from edbob.db.perms import has_permission
+
+
+class needs_perm(object):
+ """
+ Decorator to be used for handler methods which should restrict access based
+ on the current user's permissions.
+ """
+
+ def __init__(self, permission, **kwargs):
+ self.permission = permission
+ self.kwargs = kwargs
+
+ def __call__(self, fn):
+ permission = self.permission
+ kw = self.kwargs
+ def wrapped(self):
+ if not self.request.current_user:
+ self.request.session['referrer'] = self.request.url_generator.current()
+ self.request.session.flash("You must be logged in to do that.", 'error')
+ return HTTPFound(location=self.request.route_url('login'))
+ if not has_permission(self.request.current_user, permission):
+ self.request.session.flash("You do not have permission to do that.", 'error')
+ home = kw.get('redirect', self.request.route_url('home'))
+ return HTTPFound(location=home)
+ return fn(self)
+ return wrapped
+
+
+def needs_user(fn):
+ """
+ Decorator for handler methods which require simply that a user be currently
+ logged in.
+ """
+
+ def wrapped(self):
+ if not self.request.current_user:
+ self.request.session['referrer'] = self.request.url_generator.current()
+ self.request.session.flash("You must be logged in to do that.", 'error')
+ return HTTPFound(location=self.request.route_url('login'))
+ return fn(self)
+ return wrapped
diff --git a/edbob/pyramid/helpers.py b/edbob/pyramid/helpers.py
new file mode 100644
index 0000000..4307bb6
--- /dev/null
+++ b/edbob/pyramid/helpers.py
@@ -0,0 +1,33 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.pyramid.helpers`` -- Template Context Helpers
+"""
+
+import datetime
+from decimal import Decimal
+
+from webhelpers.html import *
+from webhelpers.html.tags import *
diff --git a/edbob/pyramid/paster_templates/__init__.py b/edbob/pyramid/paster_templates/__init__.py
new file mode 100644
index 0000000..7da2140
--- /dev/null
+++ b/edbob/pyramid/paster_templates/__init__.py
@@ -0,0 +1,37 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.pyramid.paster_templates`` -- Paster Templates
+"""
+
+from paste.util.template import paste_script_template_renderer
+from pyramid.paster import PyramidTemplate
+
+
+class EdbobPyramidTemplate(PyramidTemplate):
+
+ _template_dir = 'edbob'
+ summary = "edbob/pyramid project"
+ template_renderer = staticmethod(paste_script_template_renderer)
diff --git a/edbob/pyramid/subscribers.py b/edbob/pyramid/subscribers.py
new file mode 100644
index 0000000..2f899f8
--- /dev/null
+++ b/edbob/pyramid/subscribers.py
@@ -0,0 +1,66 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.pyramid.subscribers`` -- Subscribers
+"""
+
+# import pyramid.threadlocal as threadlocal
+from pyramid import threadlocal
+from pyramid.exceptions import ConfigurationError
+
+from akhet.urlgenerator import URLGenerator
+
+import edbob
+from edbob.pyramid import helpers
+
+
+def add_request_attributes(event):
+ """
+ Adds goodies to the ``request`` object.
+ """
+
+ request = event.request
+ context = request.context
+ url_generator = URLGenerator(context, request, qualified=True)
+ request.url_generator = url_generator
+
+
+def add_renderer_globals(event):
+ """
+ Adds goodies to the global template renderer context.
+ """
+
+ renderer_globals = event
+ renderer_globals['h'] = helpers
+ request = event.get('request') or threadlocal.get_current_request()
+ if not request:
+ return
+ tmpl_context = request.tmpl_context
+ try:
+ renderer_globals['session'] = request.session
+ except ConfigurationError:
+ pass
+ renderer_globals['url'] = request.url_generator
+ renderer_globals['edbob'] = edbob
diff --git a/edbob/sqlalchemy.py b/edbob/sqlalchemy.py
new file mode 100644
index 0000000..061997b
--- /dev/null
+++ b/edbob/sqlalchemy.py
@@ -0,0 +1,39 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.sqlalchemy`` -- SQLAlchemy Stuff
+"""
+
+
+def getset_factory(collection_class, proxy):
+ """
+ Helper function, useful for SQLAlchemy's "association proxy" configuration.
+ """
+
+ def getter(obj):
+ if obj is None: return None
+ return getattr(obj, proxy.value_attr)
+ setter = lambda obj, val: setattr(obj, proxy.value_attr, val)
+ return getter, setter
diff --git a/edbob/times.py b/edbob/times.py
index 4dc863a..226a489 100644
--- a/edbob/times.py
+++ b/edbob/times.py
@@ -2,23 +2,23 @@
# -*- coding: utf-8 -*-
################################################################################
#
-# edbob -- Pythonic software framework
-# Copyright © 2010,2011,2012 Lance Edgar
+# 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 General Public License as published by the Free Software
-# Foundation, either version 3 of the License, or (at your option) any later
-# version.
+# 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 General Public License for more
-# details.
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
#
-# You should have received a copy of the GNU General Public License along with
-# edbob. If not, see .
+# You should have received a copy of the GNU Affero General Public License
+# along with edbob. If not, see .
#
################################################################################
@@ -30,7 +30,7 @@ import datetime
import pytz
-__all__ = ['local_time', 'set_timezone']
+__all__ = ['local_time', 'set_timezone', 'utc_time']
_timezone = None
@@ -82,3 +82,16 @@ def set_timezone(tz):
_timezone = None
else:
_timezone = pytz.timezone(tz)
+
+
+def utc_time(timestamp=None):
+ """
+ Returns a timestamp whose ``tzinfo`` member is set to the UTC timezone.
+
+ If ``timestamp`` is not provided, then ``datetime.datetime.utcnow()`` will
+ be called to obtain the value.
+ """
+
+ if timestamp is None:
+ timestamp = datetime.datetime.utcnow()
+ return pytz.utc.localize(timestamp)
diff --git a/edbob/util.py b/edbob/util.py
new file mode 100644
index 0000000..9a4a9ba
--- /dev/null
+++ b/edbob/util.py
@@ -0,0 +1,55 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.util`` -- Utilities
+"""
+
+import edbob
+
+
+class requires_impl(edbob.Object):
+ """
+ Decorator for properties or methods defined on parent classes only for
+ documentation's sake, but which in fact rely on the derived class entirely
+ for implementation.
+
+ This merely adds a helpful message to the ``NotImplementedError`` exception
+ which will be raised.
+ """
+
+ is_property = False
+
+ def __call__(self, func):
+ if self.is_property:
+ message = "Please define the %s.%s attribute"
+ else:
+ message = "Please implement the %s.%s() method"
+
+ def wrapped(self, *args, **kwargs):
+ msg = message % (self.__class__.__name__, func.__name__)
+ msg += " (within the %s module)" % self.__class__.__module__
+ raise NotImplementedError(msg)
+
+ return wrapped
diff --git a/edbob/wx/GenerateUuid.py b/edbob/wx/GenerateUuid.py
new file mode 100644
index 0000000..76c497d
--- /dev/null
+++ b/edbob/wx/GenerateUuid.py
@@ -0,0 +1,95 @@
+#!/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 .
+#
+################################################################################
+
+import wx
+
+# begin wxGlade: extracode
+# end wxGlade
+
+import edbob
+
+
+class GenerateUuidDialog(wx.Dialog):
+ def __init__(self, *args, **kwds):
+ # begin wxGlade: GenerateUuidDialog.__init__
+ kwds["style"] = wx.DEFAULT_DIALOG_STYLE
+ wx.Dialog.__init__(self, *args, **kwds)
+ self.label_1 = wx.StaticText(self, -1, "&UUID:")
+ self.Uuid = wx.TextCtrl(self, -1, "", style=wx.TE_READONLY)
+ self.Generate = wx.Button(self, -1, "&Generate UUID")
+ self.Close = wx.Button(self, wx.ID_OK, "&Close")
+
+ self.__set_properties()
+ self.__do_layout()
+
+ self.Bind(wx.EVT_BUTTON, self.OnGenerateButton, self.Generate)
+ # end wxGlade
+
+ self.GenerateUuid()
+
+ def __set_properties(self):
+ # begin wxGlade: GenerateUuidDialog.__set_properties
+ self.SetTitle("Generate UUID")
+ self.Uuid.SetMinSize((300, -1))
+ # end wxGlade
+
+ def __do_layout(self):
+ # begin wxGlade: GenerateUuidDialog.__do_layout
+ sizer_1 = wx.BoxSizer(wx.VERTICAL)
+ sizer_2 = wx.BoxSizer(wx.VERTICAL)
+ sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
+ sizer_2.Add(self.label_1, 0, 0, 0)
+ sizer_2.Add(self.Uuid, 0, wx.TOP|wx.EXPAND, 5)
+ sizer_3.Add(self.Generate, 0, 0, 0)
+ sizer_3.Add(self.Close, 0, wx.LEFT, 10)
+ sizer_2.Add(sizer_3, 0, wx.TOP|wx.ALIGN_CENTER_HORIZONTAL, 10)
+ sizer_1.Add(sizer_2, 1, wx.ALL|wx.EXPAND, 10)
+ self.SetSizer(sizer_1)
+ sizer_1.Fit(self)
+ self.Layout()
+ self.Centre()
+ # end wxGlade
+
+ def OnGenerateButton(self, event): # wxGlade: GenerateUuidDialog.
+ self.GenerateUuid()
+ event.Skip()
+
+ def GenerateUuid(self):
+ self.Uuid.SetValue(edbob.get_uuid())
+ self.Uuid.SetSelection(-1, -1)
+ self.Uuid.SetFocus()
+
+# end of class GenerateUuidDialog
+
+
+def main():
+ app = wx.PySimpleApp()
+ dlg = GenerateUuidDialog(None)
+ dlg.ShowModal()
+ dlg.Destroy()
+ app.MainLoop()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/edbob/wx/__init__.py b/edbob/wx/__init__.py
new file mode 100644
index 0000000..1ee760f
--- /dev/null
+++ b/edbob/wx/__init__.py
@@ -0,0 +1,87 @@
+#!/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 .
+#
+################################################################################
+
+"""
+``edbob.wx`` -- wxPython Framework
+"""
+
+from __future__ import absolute_import
+
+import wx
+
+
+class ProgressDialog(wx.ProgressDialog):
+
+ def __init__(self, parent, message, maximum, title="Processing...", can_abort=True, *args, **kwargs):
+ style = wx.PD_SMOOTH|wx.PD_AUTO_HIDE|wx.PD_APP_MODAL|wx.PD_ELAPSED_TIME|wx.PD_REMAINING_TIME
+ if can_abort:
+ style |= wx.PD_CAN_ABORT
+ if 'style' in kwargs:
+ style &= kwargs['style']
+ kwargs['style'] = style
+ wx.ProgressDialog.__init__(self, title, message, maximum=maximum, parent=parent, *args, **kwargs)
+
+ def update(self, value, *args, **kwargs):
+ if not wx.ProgressDialog.Update(self, value, *args, **kwargs)[0]:
+ if self.ConfirmAbort():
+ return False
+ self.Resume()
+ return True
+
+ def destroy(self):
+ self.Destroy()
+
+ def ConfirmAbort(self):
+ dlg = wx.MessageDialog(self, "Do you really wish to cancel this process?",
+ "Really Cancel?", wx.ICON_QUESTION|wx.YES_NO|wx.NO_DEFAULT)
+ res = dlg.ShowModal()
+ dlg.Destroy()
+ return res == wx.ID_YES
+
+
+class ProgressFactory(object):
+
+ def __init__(self, parent, *args, **kwargs):
+ self.parent = parent
+ self.args = args
+ self.kwargs = kwargs
+
+ def __call__(self, message, maximum, *args, **kwargs):
+ message = '%s ...' % message
+ args = self.args + args
+ _kwargs = self.kwargs.copy()
+ _kwargs.update(kwargs)
+ return ProgressDialog(self.parent, message, maximum, *args, **_kwargs)
+
+
+def LaunchDialog(dialog_class):
+ """
+ Creates a ``wx.PySimpleApp``, then instantiates ``dialog_class`` and shows
+ it modally.
+ """
+ app = wx.PySimpleApp()
+ dlg = dialog_class(None)
+ dlg.ShowModal()
+ dlg.Destroy()
+ app.MainLoop()
diff --git a/edbob/wx/wxglade/GenerateUuid.wxg b/edbob/wx/wxglade/GenerateUuid.wxg
new file mode 100644
index 0000000..ab5eb81
--- /dev/null
+++ b/edbob/wx/wxglade/GenerateUuid.wxg
@@ -0,0 +1,65 @@
+
+
+
+
+
+
diff --git a/setup.py b/setup.py
index 10ca102..30046d3 100644
--- a/setup.py
+++ b/setup.py
@@ -2,23 +2,23 @@
# -*- coding: utf-8 -*-
################################################################################
#
-# edbob -- Pythonic software framework
-# Copyright © 2010,2011,2012 Lance Edgar
+# 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 General Public License as published by the Free Software
-# Foundation, either version 3 of the License, or (at your option) any later
-# version.
+# 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 General Public License for more
-# details.
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
#
-# You should have received a copy of the GNU General Public License along with
-# edbob. If not, see .
+# You should have received a copy of the GNU Affero General Public License
+# along with edbob. If not, see .
#
################################################################################
@@ -45,7 +45,7 @@ setup(
author = "Lance Edgar",
author_email = "lance@edbob.org",
url = "http://edbob.org/",
- license = "GNU GPL v3",
+ license = "GNU Affero GPL v3",
description = "Pythonic Software Framework",
long_description = readme,
@@ -56,7 +56,7 @@ setup(
'Environment :: Win32 (MS Windows)',
'Environment :: X11 Applications',
'Intended Audience :: Developers',
- 'License :: OSI Approved :: GNU General Public License (GPL)',
+ 'License :: OSI Approved :: GNU Affero General Public License v3',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
@@ -93,10 +93,48 @@ setup(
#
# package # low high
+ 'progressbar', # 2.3
'pytz', # 2012b
],
+ # extras_require = {
+ # #
+ # # Same guidelines apply to the extra dependencies:
+
+ # 'db': [
+ # #
+ # # package # low high
+ # #
+ # 'SQLAlchemy', # 0.6.7
+ # 'sqlalchemy-migrate', # 0.6.1
+ # ],
+
+ # 'pyramid': [
+ # #
+ # # package # low high
+ # #
+
+ # # Pyramid 1.3 introduced 'pcreate' command (and friends) to replace
+ # # deprecated 'paster create' (and friends).
+ # 'pyramid>=1.3a1', # 1.3b2
+ # ],
+ # },
+
packages = find_packages(),
include_package_data = True,
zip_safe = False,
+
+ entry_points = """
+
+[console_scripts]
+edbob = edbob.commands:main
+
+[gui_scripts]
+edbobw = edbob.commands:main
+
+[edbob.commands]
+shell = edbob.commands:ShellCommand
+uuid = edbob.commands:UuidCommand
+
+""",
)