New documentation
parent
14c8c28b9a
commit
345333614c
|
@ -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
|
|
@ -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
|
|
@ -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/
|
|
@ -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
|
|
@ -6,19 +6,41 @@
|
|||
Welcome to Milla's documentation!
|
||||
=================================
|
||||
|
||||
.. automodule:: milla
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
rationale
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
getting-started
|
||||
advanced
|
||||
changelog
|
||||
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`_.
|
||||
|
||||
|
@ -29,5 +51,6 @@ Indices and tables
|
|||
* :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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue