Commits

Dustin Hatch  committed 285e540

New documentation

  • Participants
  • Parent commits 08b6066

Comments (0)

Files changed (6)

File doc/advanced.rst

+=================
+Advanced Features
+=================
+
+*Milla* contains several powerful tools that allow web developers complete
+control over how their applications behave.
+
+.. contents:: Contents
+   :local:
+
+Propagating Configuration
+=========================
+
+While one possible way for :term:`controller` callables to obtain configuration
+information would be for them to read it each time a request is made, it would
+be extremely inefficient. To help with this, *Milla* provides a simple
+configuration dictionary that can be populated when the
+:py:class:`~milla.app.Application` is created and will be available to
+controllers as the :py:attr:`~milla.Request.config` attribute of the request.
+
+.. code-block:: python
+
+   def controller(request):
+       if request.config['t_virus'] == 'escaped':
+           return 'Zombies!'
+       else:
+           return 'Raccoon City is safe, for now'
+
+   router = milla.dispatch.routing.Router()
+   router.add_route('/', controller)
+   application = milla.Application(router)
+   application.config['t_virus'] = 'contained'
+
+*Milla* provides a simple utility called :py:func:`~milla.util.read_config`
+that can produce a flat dictionary from a standard configuration file:
+
+.. code-block:: ini
+
+   ; umbrella.ini
+   [t_virus]
+   status = escaped
+
+.. code-block:: python
+ 
+   # app.py
+   class Root(object):
+   
+       def __call__(self, request):
+           if request.config['t_virus.status'] == 'escaped':
+               return 'Zombies!'
+           else:
+               return 'Raccoon City is safe, for now'
+    
+   application = milla.Application(Root())
+   application.config.update(read_config('umbrella.ini'))
+
+Notice that the section name appears in the dictionary key as well as the
+option name, separated by a dot (``.``). This allows you to specify have
+multiple options with the same name, as long as they are in different sections.
+
+Allowing Various HTTP Methods
+=============================
+
+By default, *Milla* will reject HTTP requests using methods other than ``GET``,
+``HEAD``, or ``OPTIONS`` by returning an `HTTP 405`_ response. If you need a
+controller callable to accept these requests, you need to explicitly specify
+which methods are allowed.
+
+To change the request methods that a controller callable accepts, use the
+:py:meth:`~milla.allow` decorator.
+
+.. code-block:: python
+
+   @milla.allow('GET', 'HEAD', 'POST')
+   def controller(request):
+       response = request.ResponseClass()
+       if request.method == 'POST':
+           release_t_virus()
+           response.text = 'The T Virus has been released. Beware of Zombies'
+           return response
+       else:
+           status = check_t_virus()
+           response.text = 'The T Virus is {0}'.format(status)
+           return response
+
+.. note:: You do not need to explicitly allow the ``OPTIONS`` method; it is
+   always allowed. If an ``OPTIONS`` request is made, *Milla* will
+   automatically create a valid response informing the user of the allowed HTTP
+   request methods for the given request path. Your controller will not be
+   called in this case.
+
+Controlling Access
+==================
+
+*Milla* provides a powerful and extensible authorization framework that can be
+used to restrict access to different parts of a web application based on
+properties of the request. This framework has two major components---request
+validators and permission requirements. To use the framework, you must
+implement a :term:`request validator` and then apply a :term:`permission
+requirement` decorator to your :py:term:`controller` callables as needed.
+
+Request Validators
+******************
+
+The default request validator (:py:class:`milla.auth.RequestValidator`) is
+likely sufficient for most needs, as it assumes that a user is associated with
+a request (via the ``user`` attribute on the :py:class:`~milla.Request` object)
+and that the user has a ``permissions`` attribute that contains a list of
+permissions the user holds.
+
+.. note:: *Milla* does not automatically add a ``user`` attribute to
+   ``Request`` instances, nor does it provide any way of determining what
+   permissions the user has. As such, you will need to handle both of these on
+   your own by utilizing the :ref:`before-after-hooks`.
+
+Request validators are classes that have a ``validate`` method that takes a
+request and optionally a permission requirement. The ``validate`` method should
+return ``None`` if the request meets the requirements or raise
+:py:exc:`~milla.auth.NotAuthorized` (or a subclass thereof) if it does not.
+This exception will be called as the controller instead of the actual
+controller if the request is not valid.
+
+If you'd like to customize the response to invalid requests or the default
+request validator is otherwise insufficient for your needs, you can create your
+own request validator. To do this, you need to do the following:
+
+1. Create a subclass of :py:class:`~milla.auth.RequestValidator` that overrides
+   :py:meth:`~milla.auth.RequestValidator.validate` method (taking care to
+   return ``None`` for valid requests and raise a subclass of
+   :py:exc:`~milla.auth.NotAuthorized` for invalid requests)
+2. Register the new request validator in the ``milla.request_validator`` entry
+   point group in your ``setup.py``
+
+   For example:
+
+   .. code-block:: python
+
+      setup(name='UmbrellaCorpWeb',
+            ...
+            entry_points={
+                'milla.request_validator': [
+                    'html_login = umbrellacorpweb.lib:RequestValidatorLogin'
+                ],
+            },
+      )
+3. Set the ``request_validator`` application config key to the entry point name
+   of the new request validator
+
+   For example:
+
+   .. code-block:: python
+      
+      application = milla.Application(Root())
+      application.config['request_validator'] = 'html_login'
+
+Permission Requirements
+***********************
+
+Permission requirements are used by request validators to check whether or not
+a request is authorized for a particular controller. Permission requirements
+are applied to controller callables by using the
+:py:meth:`~milla.auth.decorators.require_perms` decorator.
+
+.. code-block:: python
+
+   class Root(object):
+
+       def __call__(self, request):
+           return 'This controller requires no permission'
+
+       @milla.require_perms('priority1')
+       def special(self, request):
+           return 'This controller requires Priority 1 permission'
+
+You can specify advanced permission requirements by using
+:py:class:`~milla.auth.permissions.Permission` objects:
+
+.. code-block:: python
+
+   class Root(object):
+
+       def __call__(self, request):
+           return 'This controller requires no permission'
+
+       @milla.require_perms(Permission('priority1') | Permission('alpha2'))
+       def special(self, request):
+           return 'This controller requires Priority 1 or Alpha 2 permission'
+
+Example
+*******
+
+The following example will demonstrate how to define a custom request validator
+that presents an HTML form to the user for failed requests, allowing them to
+log in:
+
+``setup.py``:
+
+.. code-block:: python
+
+   from setuptools import setup
+
+   setup(name='MyMillaApp',
+         version='1.0',
+         install_requires='Milla',
+         py_modules=['mymillaapp'],
+         entry_points={
+             'milla.request_validator': [
+                 'html_login = mymillaapp:RequestValidatorLogin',
+             ],
+         },
+   )
+
+``mymillaapp.py``:
+
+.. code-block:: python
+
+   import milla
+   import milla.auth
+
+   class NotAuthorizedLogin(milla.auth.NotAuthorized):
+
+       def __call__(self, request):
+           response = request.ResponseClass()
+           response.text = '''\
+   <!DOCTYPE html>
+   <html lang="en">
+   <head>
+     <title>Please Log In</title>
+     <meta charset="UTF-8">
+   </head>
+   <body>
+   <h1>Please Log In</h1>
+   <div style="color: #ff0000;">{error}</div>
+   <form action="login" method="post">
+   <div>Username:</div>
+   <div><input type="text" name="username"></div>
+   <div>Password:</div>
+   <div><input type="password" name="password"></div>
+   <div><button type="submit">Submit</button></div>
+   </form>
+   </body>
+   </html>'''.format(error=self)
+           response.status_int = 401
+           response.headers['WWW-Authenticate'] = 'HTML-Form'
+           return response
+
+   class RequestValidatorLogin(milla.auth.RequestValidator):
+
+       exc_class = NotAuthorizedLogin
+
+   class Root(object):
+
+       def __before__(self, request):
+           # Actually determining the user from the request is beyond the
+           # scope of this example. You'll probably want to use a cookie-
+           # based session and a database for this.
+           request.user = get_user_from_request(request)
+
+       @milla.require_perms('kill_zombies')
+       def kill_zombies(self, request):
+           response = request.ResponseClass()
+           response.text = 'You can kill zombies'
+           return response
+       
+       def __call__(self, request):
+           response = request.ResponseClass()
+           response.text = "Nothing to see here. No zombies, that's for sure"
+           return response
+
+   application = milla.Application(Root())
+
+.. _HTTP 405: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.6

File doc/changelog.rst

+==========
+Change Log
+==========
+
+
+0.2
+===
+
+* Python 3 support
+* Added new utility functions:
+
+  * :py:func:`~milla.util.http_date`
+  * :py:func:`~milla.util.read_config`
+
+* Added :py:meth:`~milla.Request.static_resource`
+* Corrected default handling of HTTP ``OPTIONS`` requests (`Issue #5`_)
+* Deprecated :py:mod:`milla.cli`
+* Deprecated :py:class:`~milla.dispatch.routing.Generator` in favor of
+  :py:meth:`~milla.Request.create_href`
+
+0.1.2
+=====
+
+* Improvements to :py:class:`~milla.controllers.FaviconController` (`Issue
+  #1`_)
+
+0.1.1
+=====
+
+* Fixed a bug when generating application-relative URLs with
+  :py:class:`~milla.routing.dispatch.URLGenerator`:
+
+0.1
+===
+
+Initial release
+
+.. _Issue #1: https://bitbucket.org/AdmiralNemo/milla/issue/1
+.. _Issue #5: https://bitbucket.org/AdmiralNemo/milla/issue/5

File doc/getting-started.rst

+===============
+Getting Started
+===============
+
+*Milla* aims to be lightweight and easy to use. As such, it provides only the
+tools you need to build your application the way you want, without imposing any
+restrictions on how to do it.
+
+.. contents:: Contents
+   :local:
+
+Milla's Components
+==================
+
+*Milla* provides a small set of components that help you build your web
+application in a simple, efficient manner:
+
+* WSGI Application wrapper
+* Two types of URL Dispatchers:
+  
+  * Traversal (like CherryPy or Pyramid)
+  * Routing (like Django or Pylons)
+
+* Authorization framework
+* Utility functions
+
+*Milla* does not provide an HTTP server, so you'll have to use one of the many
+implementations already available, such as `Meinheld`_ or `Paste`_, or another
+application that understands `WSGI`_, like `Apache HTTPD`_ with the `mod_wsgi`_
+module.
+
+``Application`` Objects
+=======================
+
+The core class in a *Milla*-based project is its
+:py:class:`~milla.app.Application` object. ``Application`` objects are used to
+set up the environment for the application and handle incoming requests.
+``Application`` instances are *WSGI* callables, meaning they implement the
+standard ``application(environ, start_response)`` signature.
+
+To set up an ``Application``, you will need a :term:`URL dispatcher`, which is
+an object that maps request paths to :term:`controller` callables.
+
+Choosing a URL Dispatcher
+=========================
+
+*Milla* provides two types of URL dispatchers by default, but you can create
+your own if neither of these suit your needs. The default dispatchers are
+modeled after the URL dispatchers of other popular web frameworks, but may have
+small differences.
+
+A *Milla* application can only have one URL dispatcher, so make sure you choose
+the one that will work for all of your application's needs.
+
+Traversal
+*********
+
+Object traversal is the simplest form of URL dispatcher, and is the default for
+*Milla* applications. Object traversal works by looking for path segments as
+object attributes, beginning with a :term:`root object` until a
+:term:`controller` is found.
+
+For example, consider the URL ``http://example.org/myapp/hello``. Assuming the
+*Milla* application is available at ``/myapp`` (which is controlled by the HTTP
+server), then the ``/hello`` portion becomes the request path. It contains only
+one segment, ``hello``. Thus, an attribute called ``hello`` on the :term:`root
+object` must be the controller that will produce a response to that request.
+The following code snippet will produce just such an object.
+
+.. code-block:: python
+
+   class Root(object):
+
+       def hello(self, request):
+          return 'Hello, world!'
+
+To use this class as the :term:`root object` for a *Milla* application, pass an
+instance of it to the :py:class:`~milla.app.Application` constructor:
+
+.. code-block:: python
+
+   application = milla.Application(Root())
+
+To create URL paths with multiple segments, such as ``/hello/world`` or
+``/umbrella/corp/bio``, the root object will need to have other objects
+corresponding to path segments as its attributes.
+
+This example uses static methods and nested classes:
+
+.. code-block:: python
+
+   class Root(object):
+   
+       class hello(object):
+           
+           @staticmethod
+           def world(request):
+               return 'Hello, world!'
+   
+   application = milla.Application(Root)
+
+This example uses instance methods to create the hierarchy at runtime:
+
+.. code-block:: python
+
+   class Root(object):
+
+       def __init__(self):
+           self.umbrella = Umbrella()
+
+   class Umbrella(object):
+
+       def __init__(self):
+           self.corp = Corp()
+   
+   class Corp(object):
+
+       def bio(self, request):
+           return 'T-Virus research facility'
+
+   application = milla.Application(Root())
+
+If an attribute with the name of the next path segment cannot be found, *Milla*
+will look for a ``default`` attribute.
+
+While the object traversal dispatch mechanism is simple, it is not very
+flexible. Because path segments correspond to Python object names, they must
+adhere to the same restrictions. This means they can only contain ASCII letters
+and numbers and the underscore (``_``) character. If you need more complex
+names, dynamic segments, or otherwise more control over the path mapping, you
+may need to use routing.
+
+Routing
+*******
+
+Routing offers more control of how URL paths are mapped to :term:`controller`
+callables, but require more specific configuration.
+
+To use routing, you need to instantiate a
+:py:class:`~milla.dispatch.routing.Router` object and then populate its routing
+table with path-to-controller maps. This is done using the
+:py:meth:`~milla.dispatch.routing.Router.add_route` method.
+
+.. code-block:: python
+
+   def hello(request):
+       return 'Hello, world!'
+
+   router = milla.dispatch.routing.Router()
+   router.add_route('/hello', hello)
+
+Aft er you've set up a ``Router`` and populated its routing table, pass it to
+the :py:class:`~milla.app.Application` constructor to use it in a *Milla*
+application:
+
+.. code-block:: python
+
+   application = milla.Application(router)
+
+Using routing allows paths to contain dynamic portions which will be passed to
+controller callables as keyword arguments.
+
+.. code-block:: python
+
+   def hello(request, name):
+       return 'Hello, {0}'.format(name)
+
+   router = milla.dispatch.routing.Router()
+   router.add_route('/hello/{name}', hello)
+   
+   application = milla.Application(router)
+
+In the above example, the path ``/hello/alice`` would map to the ``hello``
+function, and would return the response ``Hello, alice`` when visited.
+
+``Router`` instances can have any number of routes in their routing table. To
+add more routes, simply call ``add_route`` for each path and controller
+combination you want to expose.
+
+.. code-block:: python
+
+   def hello(request):
+       return 'Hello, world!'
+    
+   def tvirus(request):
+       return 'Beware of zombies'
+    
+   router = milla.dispatch.routing.Router()
+   router.add_route('/hello', hello)
+   router.add_route('/hello-world', hello)
+   router.add_route('/umbrellacorp/tvirus', tvirus)
+
+Controller Callables
+====================
+
+*Controller callables* are where most of your application's logic will take
+place. Based on the :abbr:`MVC (Model, View, Controller)` pattern, controllers
+handle the logic of interaction between the user interface (the *view*) and the
+data (the *model*). In the context of a *Milla*-based web application,
+controllers take input (the HTTP request, represented by a
+:py:class:`~milla.Request` object) and deliver output (the HTTP response,
+represented by a :py:class:`~milla.Response` object).
+
+Once you've decided which URL dispatcher you will use, it's time to write
+controller callables. These can be any type of Python callable, including
+functions, instance methods, classmethods, or partials. *Milla* will
+automatically determine the callable type and call it appropriately for each
+controller callable mapped to a request path.
+
+This example shows a controller callable as a function (using routing):
+
+.. code-block:: python
+
+   def index(request):
+       return 'this is the index page'
+
+   def hello(request):
+       return 'hello, world'
+
+   router = milla.dispatch.routing.Router()
+   router.add_route('/', index)
+   router.add_route('/hello', hello)
+   application = milla.Application(router)
+
+This example is equivalent to the first, but shows a controller callable as a
+class instance (using traversal):
+
+.. code-block:: python
+
+   class Controller(object):
+
+       def __call__(self, request):
+           return 'this is the index page'
+
+       def hello(self, request):
+           return 'hello, world'
+
+   application = milla.Application(Controller())
+
+Controller callables must take at least one argument, which will be an instance
+of :py:class:`~milla.Request` representing the HTTP request that was made by
+the user. The ``Request`` instance wraps the *WSGI* environment and exposes all
+of the available information from the HTTP headers, including path, method
+name, query string variables, POST data, etc.
+
+If you are using `Routing`_ and have routes with dynamic path segments, these
+segments will be passed by name as keyword arguments, so make sure your
+controller callables accept the same keywords.
+
+.. _before-after-hooks:
+
+Before and After Hooks
+**********************
+
+You can instruct *Milla* to perform additional operations before and after the
+controller callable is run. This could, for example, create a `SQLAlchemy`_
+session before the controller is called and roll back any outstanding
+transactions after it completes.
+
+To define the before and after hooks, create an ``__before__`` and/or an
+``__after__`` attribute on your controller callable. These attributes should be
+methods that take exactly one argument: the request. For example:
+
+.. code-block:: python
+
+   def setup(request):
+        request.user = 'Alice'
+    
+   def teardown(request):
+        del request.user
+    
+   def controller(request):
+       return 'Hello, {user}!'.format(user=request.user)
+   controller.__before__ = setup
+   controller.__after__ = teardown
+
+To simplify this, *Milla* handles instance methods specially, by looking for
+the ``__before__`` and ``__after__`` methods on the controller callable's class
+as well as itself.
+
+.. code-block:: python
+
+   class Controller(object):
+    
+       def __before__(self, request):
+           request.user = 'Alice'
+       
+       def __after__(self, request):
+           del request.user
+     
+       def __call__(self, request):
+           return 'Hello, {user}'.format(user=request.user)
+
+Returing a Response
+===================
+
+Up until now, the examples have shown :term:`controller` callables returning a
+string. This is the simplest way to return a plain HTML response; *Milla* will
+automatically send the appropriate HTTP headers for you in this case. If,
+however, you need to send special headers, change the content type, or stream
+data instead of sending a single response, you will need to return a
+:py:class:`~milla.Response` object. This object contains all the properties
+necessary to instruct *Milla* on what headers to send, etc. for your response.
+
+To create a :py:class:`~milla.Response` instance, use the
+:py:attr:`~milla.Request.ResponseClass` attribute from the request:
+
+.. code-block:: python
+
+   def controller(request):
+       response = request.ResponseClass()
+       response.content_type = 'text/plain'
+       response.text = 'Hello, world!'
+       return response
+
+.. _Meinheld: http://meinheld.org/
+.. _Paste: http://pythonpaste.org/
+.. _WSGI: http://www.python.org/dev/peps/pep-0333/
+.. _Apache HTTPD: http://httpd.apache.org/ 
+.. _mod_wsgi: http://code.google.com/p/modwsgi/
+.. _SQLAlchemy: http://www.sqlalchemy.org/

File doc/glossary.rst

+========
+Glossary
+========
+
+.. glossary::
+
+   controller
+   controller callable
+      A callable that accepts a :py:class:`~milla.Request` instance and any
+      optional parameters and returns a response
+
+   permission requirement
+      A set of permissions required to access a particular URL path. Permission
+      requirements are specified by using the
+      :py:meth:`~milla.auth.require_perm` decorator on a restricted
+      :term:`controller callable`
+
+   request validator
+      A function that checks a request to ensure it meets the specified
+      :term:`permission requirement` before calling a :term:`controller
+      callable`
+
+   root object
+      The starting object in the object traversal URL dispatch mechanism from
+      which all path lookups are performed
+
+   URL dispatcher
+      An object that maps request paths to :term:`controller` callables

File doc/index.rst

 Welcome to Milla's documentation!
 =================================
 
-.. automodule:: milla
-
 Contents:
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 1
    
-   rationale
+   getting-started
+   advanced
+   changelog
+   reference/index
+   glossary
 
-.. toctree::
-   :maxdepth: 1
+*Milla* is a simple and lightweight web framework for Python. It built on top
+of `WebOb`_ and thus implements the `WSGI`_ standard. It aims to be easy to use
+while imposing no restrictions, allowing web developers to write code the way
+they want, using the tools, platform, and extensions they choose.
 
-   reference/index
+Example
+=======
+
+.. code:: python
+
+    from wsgiref import simple_server
+    from milla.dispatch import routing
+    import milla
+
+
+    def hello(request):
+        return 'Hello, world!'
+
+    router = routing.Router()
+    router.add_route('/', hello)
+    app = milla.Application(router)
+
+    httpd = simple_server.make_server('', 8080, app)
+    httpd.serve_forever()
 
 *Milla* is released under the terms of the `Apache License, version 2.0`_.
 
 * :ref:`modindex`
 * :ref:`search`
 
-
+.. _WebOb: http://webob.org/
+.. _WSGI: http://wsgi.readthedocs.org/
 .. _Apache License, version 2.0: http://www.apache.org/licenses/LICENSE-2.0

File doc/rationale.rst

-=========
-Rationale
-=========
-
-As of early 2011, there is a lot of flux in the Python world with
-regard to web frameworks. There are a couple of big players, namely
-`Django`_, `Pylons`_, and `TurboGears`_, as well as several more
-obscure projects like `CherryPy`_ and `Bottle`_. Having worked with
-many of these projects, I decided that although each has its strengths,
-they all also had something about them that just made me feel
-uncomfortable working with them.
-
-Framework Comparison
-====================
-
-Django
-++++++
-
-.. rubric:: Strengths
-
-* Very popular and actively developed, making it easy to get help and
-  solve problems
-* Fully-featured, including an Object-Relational Mapper, URL dispatcher,
-  template engine, and form library. Also includes "goodies" like
-  authentication, an "admin" application, and sessions
-
-.. rubric:: Discomforts
-
-I am not specifically listing any of these issues as weaknesses or
-drawbacks, because they aren't *per-se*. Honestly, there isn't anything
-wrong with Django, and many people love it. Personally, I don't feel
-comfortable working with it for a few reasons.
-
-* Storing configuration in a Python module is absurd
-* All of the components are so tightly-integrated it is nearly
-  impossible to use some pieces without the others.
-  
-  * I really don't like its ORM. `SQLAlchemy`_ is tremendously more
-    powerful, and isn't nearly as restrictive (naming conventions, etc.)
-  * The session handling middleware is very limited in comparison to
-    other projects like `Beaker`_
-  * I am not fond of the template engine and would prefer to use
-    `Genshi`_.
-
-Pylons/Pyramid
-++++++++++++++
-
-The original Pylons was a very powerful web framework. It was probably
-my favorite framework, and I have built a number of applications using
-it. Unfortunately, development has been discontinued and efforts are
-now being concentrated on `Pyramid`_ instead.
-
-Pylons
-------
-
-.. rubric:: Strengths
-
-* While not as popular as Django, there still a significant following
-* The  code base is very decoupled, allowing developers to swap out
-  components without affecting the overall functionality of the
-  framework.
-
-.. rubric:: Weaknesses
-
-* Overutilization of StackedObject proxies and global variables
-
-Pyramid
--------
-
-I simply do not like Pyramid at all, and it is really disappointing that
-the Pylons project has moved in this direction. Essentially everything
-that I liked about Pylons is gone. The idea of using *traversal* to map
-URLs to routines is clever, but it is overly complex compared to the
-familiar URL dispatching in other frameworks. 
-
-* Tightly integrated with several Zope components, mostly interfaces
-  (*puke*)
-* Template renderers are insanely complex and again, I don't like Zope
-  interfaces. There is no simple way to use Genshi, which I absolutely
-  adore.
-
-
-Other Frameworks
-++++++++++++++++
-
-I haven't used the other frameworks as much. In general, I try to avoid
-having my applications depend on obscure or unmaintained libraries
-because when I find a bug (and I will), I need some assurance that it
-will be fixed soon. I do not like having to patch other people's code
-in production environments, especially if it is an application I am 
-passing along to a client.
-
-I never really looked at TurboGears at all, and with the recent changes
-to the Pylons project, upon which TurboGears is based, there is a great
-deal of uncertainty with regard to its future.
-
-CherryPy is very nice, and I did a bit of work with it a while back. I
-thought it was dead for a long time, though, and I have never really
-produced a production application built on it. With its most recent
-release (3.2.0), it is the first web framework to support Python 3,
-which is exciting. I may revisit it in the near future, as a matter
-of fact.
-
-The Truth
-=========
-
-The truth is, I started *Milla* as an exercise to better understand
-WSGI. All of the frameworks discussed above are great, and will most
-likely serve everyone's needs. There really isn't any reason for anyone
-to use *Milla* over any of them, but I won't stop you.
-
-.. _Django: http://www.djangoproject.com/
-.. _Pylons: http://pylonshq.com/
-.. _TurboGears: http://www.turbogears.org/
-.. _CherryPy: http://www.cherrypy.org/
-.. _Bottle: http://bottlepy.org/
-.. _SQLAlchemy: http://www.sqlalchemy.org/
-.. _Beaker: http://beaker.groovie.org/
-.. _Genshi: http://genshi.edgewall.org/
-.. _Pyramid: http://docs.pylonsproject.org/projects/pyramid/1.0/index.html