"""This class encapsulates the main flow of control for Aquarium."""
__docformat__ = "restructuredtext"
# Created: Tue Sep 12 21:33:55 PDT 2000
# Author: Shannon -jj Behrens
# Email: jjinux@users.sourceforge.net
#
# Copyright (c) Shannon -jj Behrens. All rights reserved.
from sys import exc_info
import aquarium.conf.AquariumProperties as properties
class Aquarium:
"""This class encapsulates the main flow of control for Aquarium.
Almost everything important here can be configured via
``AquariumProperties``. Nonetheless, if you still need more control, feel
free to subclass this class. In the entry point of your Web server, you'll
need to use your class instead of this class. To replace some of the core
Aquarium classes, subclass this, and override the appropriate factory
methods of below. The ``__call__``, ``getInitAndCleanupTaskList``, and
``afterInit`` methods are also very appropriate for extending or
overriding.
The following private variables are used:
_ctx
This is our reference to ``ctx``.
_prevExc, _prevExcInfo
These are used by ``screenLoop`` and ``catchDoubleException``.
"""
def __init__(self, wsadaptor):
"""Accept ``wsadaptor``, and prepare for real work.
This also calls aquarium.util.InternalLibrary.clearModules_.
.. _aquarium.util.InternalLibrary.clearModules:
aquarium.util.InternalLibrary-module.html#clearModules
"""
if not hasattr(properties, "CLEAR_MODULES"):
raise AttributeError("""\
No properties.CLEAR_MODULES attribute. Is the packagePath correctly set?
See aquarium.PackagePath.""")
from aquarium.util.InternalLibrary import clearModules
clearModules()
ctx = self._ctx = self.createContext()
ctx.wsa = wsadaptor
ctx.iLib = self.createInternalLibrary()
def createContext(self):
from aquarium.util.Context import Context
return Context()
def createInternalLibrary(self):
from aquarium.util.InternalLibrary import InternalLibrary
return InternalLibrary(self._ctx)
def __call__(self):
"""Handle the request.
This encapsulates the main flow of control. It is not a violation of
encapsulation to read the source of this method.
See tryFinally_.
.. _aquarium.util.TryFinally.tryFinally:
aquarium.util.TryFinally-module.html#tryFinally
"""
from TryFinally import tryFinally
tryFinally(self.getInitAndCleanupTaskList())
def getInitAndCleanupTaskList(self):
"""Return all the init and cleanup methods in a list of tuples.
This encapsulates the main flow of control. It is not a violation of
encapsulation to read the source of this method.
See tryFinally_.
.. _aquarium.util.TryFinally.tryFinally:
aquarium.util.TryFinally-module.html#tryFinally
"""
assert (not hasattr(self, "initAll") and
not hasattr(self, "cleanupAll")), """
The initAll and cleanupAll methods are no longer supported because they could
not guarantee proper resource cleanup. Extending
aquarium.util.Aquarium.getInitAndCleanupTaskList is the new way to do this."""
return [
((), (self.cleanupContext,) ),
((self.initCookies,), ),
((self.initUrlScheme,), ),
((self.initForm,), ),
((self.initDatabase,), (self.cleanupDatabase,) ),
((self.initSession,), (self.cleanupSession,) ),
((self.initGetText,), ),
((self.afterInit,), ),
]
def initCookies(self):
"""Get data from cookies, or start a new one.
Add the following to ``self._ctx``: ``cookiesVerified``,
``request.cookie``, ``response.cookie``.
"""
ctx = self._ctx
ctx.cookiesVerified = 0
if properties.USE_COOKIES:
import Cookie
ctx.request.cookie = Cookie.SimpleCookie()
ctx.response.cookie = Cookie.SimpleCookie()
cookieValue = ctx.wsa.getCgiEnv().get("HTTP_COOKIE")
if cookieValue:
ctx.request.cookie.load(cookieValue)
ctx.cookiesVerified = 1
def initUrlScheme(self):
"""Initialize the urlscheme instance.
Add the following to ``self._ctx``: ``url`` (an urlscheme).
"""
ctx = self._ctx
ctx.url = ctx.iLib.aquariumFactory("urlscheme." + properties.URL_SCHEME)
def initForm(self):
"""Retrieve the data from the form.
If an ``aquarium.util.InternalLibrary.FormValueError`` is raised,
direct the user to the exception screen.
Add the following to ``self._ctx``: ``form`` (an instance of
aquarium.util.FormDict_).
.. _aquarium.util.FormDict: aquarium.util.FormDict.FormDict-class.html
"""
from aquarium.util.FormDict import FormDict
from aquarium.util.InternalLibrary import FormValueError
ctx = self._ctx
try:
ctx.form = FormDict(ctx.wsa.getForm())
except FormValueError:
self._ctx.form = {}
self._ctx.screen = "exception"
self._ctx.args = (exc_info(),)
self._ctx.kargs = {}
def initDatabase(self):
"""Connect to the database.
Add the following to ``self._ctx``: ``dba``, ``db`` (via
aquarium.database.DatabaseAssistant_).
.. _aquarium.database.DatabaseAssistant:
aquarium.database.DatabaseAssistant.DatabaseAssistant-class.html
"""
ctx = self._ctx
if properties.USE_DATABASE:
dba = ctx.dba = ctx.iLib.aquariumFactory(
"database." + properties.AQUARIUM_DATABASE_ASSISTANT_MODULE)
dba.connect()
def cleanupDatabase(self):
"""Close the connection to the database."""
if properties.USE_DATABASE:
self._ctx.dba.close()
def initSession(self):
"""Get data from session, or start a new one.
Some session containers subclass aquarium.util.AquariumClass, and some
don't. Pass a ``ctx`` as necessary.
Add the following to ``self._ctx``: ``session``.
"""
ctx = self._ctx
if properties.USE_SESSIONS:
if (properties.USE_COOKIES and properties.USE_SESSION_COOKIES and
ctx.request.cookie.has_key("sid")):
sid = ctx.request.cookie["sid"].value
else:
sid = ctx.form.get("sid")
sessionModule = "session." + properties.SESSION_CONTAINER_MODULE
try:
sessionContainer = ctx.iLib.aquariumFactory(
sessionModule, aquariumFactoryNoContext=1)
except TypeError:
sessionContainer = ctx.iLib.aquariumFactory(sessionModule)
ctx.session = sessionContainer.open(sid)
if properties.USE_COOKIES and properties.USE_SESSION_COOKIES:
self.initSessionCookie()
def initSessionCookie(self):
"""Initialize the session cookie.
This is called by ``initSession``.
If HTTPS is currently being used and ``properties.USE_SECURE_COOKIES``
is ``True`` (the default), the secure attribute will be set. This is
good if you're worried about security, but bad if you bounce back and
forth between HTTP and HTTPS.
"""
from urlparse import urlparse
ctx = self._ctx
PATH = 2
path = urlparse(ctx.url.getRootUrl())[PATH]
ctx.response.cookie["sid"] = ctx.session["sid"]
ctx.response.cookie["sid"]["path"] = path
expiration = ctx.session.getCookieExpiration()
if expiration:
ctx.response.cookie["sid"]["expires"] = expiration
if (ctx.url.getDefaultScheme() == "https://" and
getattr(properties, "USE_SECURE_COOKIES", True)):
ctx.response.cookie["sid"]["secure"] = "secure"
def cleanupSession(self):
"""Call ``save`` on the session."""
if properties.USE_SESSIONS:
self._ctx.session.save()
def initGetText(self):
"""Initialize gettext.
Use the class API so that "_" is not globally installed.
Add the following to ``self._ctx``: ``translation``, ``_``,
``gettext``, ``ngettext``. If ``properties.USE_GETTEXT`` is False,
``gettext.NullTranslations``. That way, your code will work even if
you use ``_()`` with gettext turned off.
"""
# It's unnecessary to cache translation instances. The gettext module
# already does that.
ctx = self._ctx
if properties.USE_GETTEXT:
from gettext import translation
ctx.translation = translation(
languages=self.getLanguagePreferences(),
*properties.GETTEXT_ARGS, **properties.GETTEXT_KWARGS)
else:
from gettext import NullTranslations
ctx.translation = NullTranslations()
ctx.gettext = ctx.translation.gettext
ctx.ngettext = ctx.translation.ngettext
ctx._ = ctx.gettext
def getLanguagePreferences(self):
"""Return a list of preferred languages, most preferred first.
The list may be empty. By default, just use parseAcceptLanguage_ on
the ``ACCEPT_LANGUAGE`` header. The list will be passed to
``filterLanguagePreferences``.
.. _parseAcceptLanguage:
aquarium.parse.AcceptLanguage-module.html#parseAcceptLanguage
"""
from aquarium.parse.AcceptLanguage import parseAcceptLanguage
acceptLanguage = self._ctx.wsa.getCgiEnv().get("HTTP_ACCEPT_LANGUAGE")
languages = parseAcceptLanguage(acceptLanguage)
self.filterLanguagePreferences(languages)
return languages
def filterLanguagePreferences(self, languages):
"""Update the ``languages`` list by applying additional logic.
If not None, use ``properties.GETTEXT_ULTIMATE_FALLBACK`` (which
defaults to "en-us") as an ultimate fallback. That means it gets
appended to the list of languages if it isn't already there. It also
means that if it's in the list of languages, everything after it is
deleted.
This deleting behavior is strange but useful. Normally, everything in
the code is in "en-us". However, the "en-us" translation catalog is
usually empty. If the user requests ``["en-us", "zh-cn"]`` and a
translation isn't found for a string in "en-us", you don't want
gettext to fallback to "zh-cn". You want it to just use the string
itself. Hence, if a string isn't found in the
``properties.GETTEXT_ULTIMATE_FALLBACK`` catalog, the string in the
source code will be used.
"""
ultimate = getattr(properties, "GETTEXT_ULTIMATE_FALLBACK", "en-us")
if not ultimate:
return
if ultimate not in languages:
languages.append(ultimate)
index = languages.index(ultimate)
languages[index+1:] = []
def cleanupContext(self):
"""Help the garbage collector by emptying ``self._ctx``.
This breaks all the circular references. By having everything garbage
collected nicely, it insures that all files, such as session files, get
closed properly.
"""
ctx = self._ctx
for i in dir(ctx):
try:
delattr(ctx, i)
except AttributeError:
pass # __getattr__ attributes can't be deleted.
def afterInit(self):
"""Continue after initialization.
This encapsulates the main flow of control. It is not a violation of
encapsulation to read the source of this method.
"""
ctx = self._ctx
generator = self.screenLoop()
if not hasattr(ctx.screenInstance, "getHeaders"):
raise AttributeError("""\
%s instance has no attribute 'getHeaders'.
This is usually caused by forgetting to forward from a controller to a view."""%
ctx.screenInstance.__class__.__name__)
ctx.wsa.writeHeaders(ctx.screenInstance.getHeaders())
for i in generator:
ctx.wsa.write(self.applyFilters(i))
def screenLoop(self):
"""Show the desired screen, and loop around to handle exceptions.
By using a loop, the same code path can be used for exceptions. In
fact:
* The aquarium.util.InternalLibrary.Forward_ exception results in
looping around to show the desired screen to forward to.
* ``ImportError``'s and ``AttributeError``'s while trying to import the
screen result in the ``not_found`` screen.
* Any other ``Exception`` results in the ``exception`` screen.
Be especially wary of infinite loops since we're catching all
exceptions and looping around. Use ``handleDoubleException`` in these
cases, but note that it too will raise an exception, by design.
If a screen runs successfully to completion, if the return value is a
generator, just return it. If it is a string, wrap it in a generator
and return it.
Add the following to ``self._ctx``: ``screenList``.
.. _aquarium.util.InternalLibrary.Forward:
aquarium.util.InternalLibrary.Forward-class.html
"""
from aquarium.util.InternalLibrary import Forward
class _Continue(Exception): pass # Can't use continue in an except.
def yield_this(arg): yield arg # Wrap arg in a generator.
MAX_FORWARDS = 1024
ctx = self._ctx
if hasattr(ctx, "screen"): # We had an exception during init.
screen, args, kargs = ctx.screen, ctx.args, ctx.kargs
else:
whichScreen = ctx.url.whichScreen()
if isinstance(whichScreen, tuple):
screen, args, kargs = whichScreen
else:
screen, args, kargs = whichScreen, (), {}
self._prevExc, self._prevExcInfo = None, None
if not screen:
screen = properties.DEFAULT_SCREEN
if not ctx.iLib.validModuleName(screen):
try: # Assertions might be turned off.
raise AssertionError("Invalid screen name: %s" % `screen`)
except:
screen, args, kargs = "not_found", (exc_info(),), {}
for _ignored in xrange(MAX_FORWARDS):
try:
ctx.screen = screen
ctx.screenList = getattr(ctx, "screenList", []) + [ctx.screen]
try:
ctx.screenInstance = ctx.iLib.aquariumFactory(
"screen." + screen)
except (ImportError, AttributeError), e:
self.catchDoubleException(e)
screen, args, kargs = "not_found", (exc_info(),), {}
raise _Continue
ret = ctx.iLib.inverseExtend(ctx.screenInstance.__call__,
*args, **kargs)
if not hasattr(ret, "next"):
ret = yield_this(ret)
return ret
except _Continue:
pass
except Forward, e:
screen, args, kargs = e.screen, e.args, e.kargs
except Exception, e:
self.catchDoubleException(e)
screen, args, kargs = "exception", (exc_info(),), {}
raise OverflowError("Too many forwards")
def catchDoubleException(self, e):
"""Call ``handleDoubleException`` as needed.
``handleDoubleException`` raises its own exception, so this method
should only return normally on the first exception.
"""
if type(self._prevExc) == type(e):
self.handleDoubleException((self._prevExcInfo, exc_info()))
else:
self._prevExc, self._prevExcInfo = e, exc_info()
def handleDoubleException(self, excInfos):
"""Handle the case of a double exception.
This happens when a normal screen raises an exception, and then later,
the exception screen raises an exception.
"""
from aquarium.util.InternalLibrary import getExceptionStr
raise Exception("""
An exception has occurred:
%s
While attempting to show the exception screen, another exception occurred:
%s
The system is unable to handle the exception at this level. Another exception
will be raised combining the previous two exceptions.""" %
tuple([getExceptionStr(*i) for i in excInfos]))
def applyFilters(self, buf):
"""Filter and return the given ``buf``.
Filters are not run from within the big try/except block because you'll
probably want filters run for the exception screen as well.
Furthermore, filters really shouldn't be expected to raise exceptions
on a daily basis.
"""
ctx = self._ctx
if properties.USE_FILTERS:
for i in properties.FILTER_MODULES:
buf = ctx.iLib.aquariumFactory("filter." + i)(buf)
return buf
|