New documentation

master
Dustin C. Hatch 2013-01-22 10:47:39 -06:00
parent 14c8c28b9a
commit 345333614c
6 changed files with 692 additions and 129 deletions

272
doc/advanced.rst Normal file
View File

@ -0,0 +1,272 @@
=================
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

39
doc/changelog.rst Normal file
View File

@ -0,0 +1,39 @@
==========
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

321
doc/getting-started.rst Normal file
View File

@ -0,0 +1,321 @@
===============
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/

28
doc/glossary.rst Normal file
View File

@ -0,0 +1,28 @@
========
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

View File

@ -6,19 +6,41 @@
Welcome to Milla's documentation! Welcome to Milla's documentation!
================================= =================================
.. automodule:: milla
Contents: Contents:
.. toctree::
:maxdepth: 2
rationale
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
getting-started
advanced
changelog
reference/index reference/index
glossary
*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.
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`_. *Milla* is released under the terms of the `Apache License, version 2.0`_.
@ -29,5 +51,6 @@ Indices and tables
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :ref:`search`
.. _WebOb: http://webob.org/
.. _WSGI: http://wsgi.readthedocs.org/
.. _Apache License, version 2.0: http://www.apache.org/licenses/LICENSE-2.0 .. _Apache License, version 2.0: http://www.apache.org/licenses/LICENSE-2.0

View File

@ -1,120 +0,0 @@
=========
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