From 345333614cf4785980520f517879d6dbc0746651 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Tue, 22 Jan 2013 10:47:39 -0600 Subject: [PATCH] New documentation --- doc/advanced.rst | 272 ++++++++++++++++++++++++++++++++++ doc/changelog.rst | 39 +++++ doc/getting-started.rst | 321 ++++++++++++++++++++++++++++++++++++++++ doc/glossary.rst | 28 ++++ doc/index.rst | 41 +++-- doc/rationale.rst | 120 --------------- 6 files changed, 692 insertions(+), 129 deletions(-) create mode 100644 doc/advanced.rst create mode 100644 doc/changelog.rst create mode 100644 doc/getting-started.rst create mode 100644 doc/glossary.rst delete mode 100644 doc/rationale.rst diff --git a/doc/advanced.rst b/doc/advanced.rst new file mode 100644 index 0000000..dd4de6d --- /dev/null +++ b/doc/advanced.rst @@ -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 = '''\ + + + + Please Log In + + + +

Please Log In

+
{error}
+
+
Username:
+
+
Password:
+
+
+
+ + '''.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 diff --git a/doc/changelog.rst b/doc/changelog.rst new file mode 100644 index 0000000..7632357 --- /dev/null +++ b/doc/changelog.rst @@ -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 diff --git a/doc/getting-started.rst b/doc/getting-started.rst new file mode 100644 index 0000000..673b09d --- /dev/null +++ b/doc/getting-started.rst @@ -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/ diff --git a/doc/glossary.rst b/doc/glossary.rst new file mode 100644 index 0000000..5ce9785 --- /dev/null +++ b/doc/glossary.rst @@ -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 diff --git a/doc/index.rst b/doc/index.rst index 1385b29..ea0c3dd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -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 diff --git a/doc/rationale.rst b/doc/rationale.rst deleted file mode 100644 index 82d0796..0000000 --- a/doc/rationale.rst +++ /dev/null @@ -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