Mostly improving documentation

This commit is contained in:
Tom Christie 2011-01-17 17:34:58 +00:00
parent b0ce3f92c6
commit 9979903272
9 changed files with 391 additions and 38 deletions

View File

@ -3,5 +3,8 @@ syntax: glob
*.pyc
*.db
env
cache
html
.project
.pydevproject
.settings

View File

@ -11,3 +11,7 @@ source ./env/bin/activate
pip install -r ./requirements.txt
python ./src/manage.py test
# To build the documentation...
sphinx-build -c docs -b html -d cache docs html

220
docs/conf.py Normal file
View File

@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
#
# Asset Platform documentation build configuration file, created by
# sphinx-quickstart on Fri Nov 19 20:24:09 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))
import settings
from django.core.management import setup_environ
setup_environ(settings)
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = []
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'FlyWheel'
copyright = u'2011, Tom Christie'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'restfulloggingdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'restfullogging.tex', u'restful logging Documentation',
u'tom c', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'restfullogging', u'restful logging Documentation',
[u'tom c'], 1)
]

12
docs/index.rst Normal file
View File

@ -0,0 +1,12 @@
FlyWheel Documentation
======================
This is the online documentation for FlyWheel - A REST framework for Django.
Indices and tables
------------------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -12,10 +12,38 @@ import re
class ModelResource(Resource):
"""A specialized type of Resource, for RESTful resources that map directly to a Django Model.
Useful things this provides:
0. Default input validation based on ModelForms.
1. Nice serialization of returned Models and QuerySets.
2. A default set of create/read/update/delete operations."""
# The model attribute refers to the Django Model which this Resource maps to.
# (The Model's class, rather than an instance of the Model)
model = None
# By default the set of returned fields will be the set of:
#
# 0. All the fields on the model, excluding 'id'.
# 1. All the properties on the model.
# 2. The absolute_url of the model, if a get_absolute_url method exists for the model.
#
# If you wish to override this behaviour,
# you should explicitly set the fields attribute on your class.
fields = None
# By default the form used with be a ModelForm for self.model
# If you wish to override this behaviour or provide a sub-classed ModelForm
# you should explicitly set the form attribute on your class.
form = None
# By default the set of input fields will be the same as the set of output fields
# If you wish to override this behaviour you should explicitly set the
# form_fields attribute on your class.
form_fields = None
def get_bound_form(self, data=None, is_response=False):
"""Return a form that may be used in validation and/or rendering an html emitter"""
if self.form:
@ -25,7 +53,7 @@ class ModelResource(Resource):
class NewModelForm(ModelForm):
class Meta:
model = self.model
fields = self.form_fields if self.form_fields else None #self.fields
fields = self.form_fields if self.form_fields else None
if data and not is_response:
return NewModelForm(data)
@ -36,7 +64,27 @@ class ModelResource(Resource):
else:
return None
def cleanup_request(self, data, form_instance):
"""Override cleanup_request to drop read-only fields from the input prior to validation.
This ensures that we don't error out with 'non-existent field' when these fields are supplied,
and allows for a pragmatic approach to resources which include read-only elements.
I would actually like to be strict and verify the value of correctness of the values in these fields,
although that gets tricky as it involves validating at the point that we get the model instance.
See here for another example of this approach:
http://fedoraproject.org/wiki/Cloud_APIs_REST_Style_Guide
https://www.redhat.com/archives/rest-practices/2010-April/thread.html#00041"""
read_only_fields = set(self.fields) - set(self.form_instance.fields)
input_fields = set(data.keys())
clean_data = {}
for key in input_fields - read_only_fields:
clean_data[key] = data[key]
return super(ModelResource, self).cleanup_request(clean_data, form_instance)
def cleanup_response(self, data):

View File

@ -1,4 +1,5 @@
import json
from rest.status import ResourceException, Status
class BaseParser(object):
def __init__(self, resource):
@ -10,7 +11,10 @@ class BaseParser(object):
class JSONParser(BaseParser):
def parse(self, input):
return json.loads(input)
try:
return json.loads(input)
except ValueError, exc:
raise ResourceException(Status.HTTP_400_BAD_REQUEST, {'detail': 'JSON parse error - %s' % str(exc)})
class XMLParser(BaseParser):
pass

View File

@ -1,39 +1,29 @@
from django.http import HttpResponse
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.http import HttpResponse
from rest import emitters, parsers
from rest.status import Status, ResourceException
from decimal import Decimal
import re
# TODO: Authentication
# TODO: Display user login in top panel: http://stackoverflow.com/questions/806835/django-redirect-to-previous-page-after-login
# TODO: Return basic object, not tuple
# TODO: Return basic object, not tuple of status code, content, headers
# TODO: Take request, not headers
# TODO: Remove self.blah munging (Add a ResponseContext object)
# TODO: Erroring on non-existent fields
# TODO: Standard exception classes and module for status codes
# TODO: Standard exception classes
# TODO: Figure how out references and named urls need to work nicely
# TODO: POST on existing 404 URL, PUT on existing 404 URL
# TODO: Authentication
#
# NEXT: Generic content form
# NEXT: Remove self.blah munging (Add a ResponseContext object?)
# NEXT: Caching cleverness
# NEXT: Test non-existent fields on ModelResources
#
# FUTURE: Erroring on read-only fields
# Documentation, Release
#
STATUS_400_BAD_REQUEST = 400
STATUS_405_METHOD_NOT_ALLOWED = 405
STATUS_406_NOT_ACCEPTABLE = 406
STATUS_415_UNSUPPORTED_MEDIA_TYPE = 415
STATUS_500_INTERNAL_SERVER_ERROR = 500
STATUS_501_NOT_IMPLEMENTED = 501
class ResourceException(Exception):
def __init__(self, status, content='', headers={}):
self.status = status
self.content = content
self.headers = headers
class Resource(object):
@ -110,13 +100,16 @@ class Resource(object):
def reverse(self, view, *args, **kwargs):
"""Return a fully qualified URI for a given view or resource.
Use the Sites framework if possible, otherwise fallback to using the current request."""
Add the domain using the Sites framework if possible, otherwise fallback to using the current request."""
return self.add_domain(reverse(view, *args, **kwargs))
def add_domain(self, path):
"""Given a path, return an fully qualified URI.
Use the Sites framework if possible, otherwise fallback to using the domain from the current request."""
# Note that out-of-the-box the Sites framework uses the reserved domain 'example.com'
# See RFC 2606 - http://www.faqs.org/rfcs/rfc2606.html
try:
site = Site.objects.get_current()
if site.domain and site.domain != 'example.com':
@ -150,7 +143,7 @@ class Resource(object):
def not_implemented(self, operation):
"""Return an HTTP 500 server error if an operation is called which has been allowed by
allowed_operations, but which has not been implemented."""
raise ResourceException(STATUS_500_INTERNAL_SERVER_ERROR,
raise ResourceException(Status.HTTP_500_INTERNAL_SERVER_ERROR,
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
@ -172,18 +165,18 @@ class Resource(object):
# if anon_user and not anon_allowed_operations raise PermissionDenied
# return
def check_method_allowed(self, method):
"""Ensure the request method is acceptable for this resource."""
if not method in self.CALLMAP.keys():
raise ResourceException(STATUS_501_NOT_IMPLEMENTED,
raise ResourceException(Status.HTTP_501_NOT_IMPLEMENTED,
{'detail': 'Unknown or unsupported method \'%s\'' % method})
if not self.CALLMAP[method] in self.allowed_operations:
raise ResourceException(STATUS_405_METHOD_NOT_ALLOWED,
raise ResourceException(Status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
def get_bound_form(self, data=None, is_response=False):
"""Optionally return a Django Form instance, which may be used for validation
and/or rendered by an HTML/XHTML emitter.
@ -207,16 +200,31 @@ class Resource(object):
By default this uses form validation to filter the basic input into the required types."""
if form_instance is None:
return data
# Default form validation does not check for additional invalid fields
non_existent_fields = []
for key in set(data.keys()) - set(form_instance.fields.keys()):
non_existent_fields.append(key)
if not form_instance.is_valid():
if not form_instance.errors:
if not form_instance.is_valid() or non_existent_fields:
if not form_instance.errors and not non_existent_fields:
# If no data was supplied the errors property will be None
details = 'No content was supplied'
else:
# Add standard field errors
details = dict((key, map(unicode, val)) for (key, val) in form_instance.errors.iteritems())
if form_instance.non_field_errors():
details['_extra'] = self.form.non_field_errors()
raise ResourceException(STATUS_400_BAD_REQUEST, {'detail': details})
# Add any non-field errors
if form_instance.non_field_errors():
details['errors'] = self.form.non_field_errors()
# Add any non-existent field errors
for key in non_existent_fields:
details[key] = ['This field does not exist']
# Bail. Note that we will still serialize this response with the appropriate content type
raise ResourceException(Status.HTTP_400_BAD_REQUEST, {'detail': details})
return form_instance.cleaned_data
@ -241,7 +249,7 @@ class Resource(object):
try:
return self.parsers[content_type]
except KeyError:
raise ResourceException(STATUS_415_UNSUPPORTED_MEDIA_TYPE,
raise ResourceException(Status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
{'detail': 'Unsupported media type \'%s\'' % content_type})
@ -295,14 +303,13 @@ class Resource(object):
(accept_mimetype == mimetype)):
return (mimetype, emitter)
raise ResourceException(STATUS_406_NOT_ACCEPTABLE,
raise ResourceException(Status.HTTP_406_NOT_ACCEPTABLE,
{'detail': 'Could not statisfy the client\'s accepted content type',
'accepted_types': [item[0] for item in self.emitters]})
def _handle_request(self, request, *args, **kwargs):
"""
Broadly this consists of the following procedure:
0. ensure the operation is permitted
@ -347,9 +354,14 @@ class Resource(object):
except ResourceException, exc:
# On exceptions we still serialize the response appropriately
(self.resp_status, ret, self.resp_headers) = (exc.status, exc.content, exc.headers)
# Fall back to the default emitter if we failed to perform content negotiation
if emitter is None:
mimetype, emitter = self.emitters[0]
mimetype, emitter = self.emitters[0]
# Provide an empty bound form if we do not have an existing form and if one is required
if self.form_instance is None and emitter.uses_forms:
self.form_instance = self.get_bound_form()

50
src/rest/status.py Normal file
View File

@ -0,0 +1,50 @@
class Status(object):
"""Descriptive HTTP status codes, for code readability."""
HTTP_200_OK = 200
HTTP_201_CREATED = 201
HTTP_202_ACCEPTED = 202
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404
HTTP_405_METHOD_NOT_ALLOWED = 405
HTTP_406_NOT_ACCEPTABLE = 406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
HTTP_408_REQUEST_TIMEOUT = 408
HTTP_409_CONFLICT = 409
HTTP_410_GONE = 410
HTTP_411_LENGTH_REQUIRED = 411
HTTP_412_PRECONDITION_FAILED = 412
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_414_REQUEST_URI_TOO_LONG = 414
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
HTTP_417_EXPECTATION_FAILED = 417
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302
HTTP_303_SEE_OTHER = 303
HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_501_NOT_IMPLEMENTED = 501
HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
class ResourceException(Exception):
def __init__(self, status, content='', headers={}):
self.status = status
self.content = content
self.headers = headers

View File

@ -4,7 +4,7 @@
from django.test import TestCase
from django.core.urlresolvers import reverse
from testapp import views
import json
#import json
#from rest.utils import xml2dict, dict2xml