diff options
| -rw-r--r-- | .hgignore | 5 | ||||
| -rw-r--r-- | requirements.txt | 3 | ||||
| -rw-r--r-- | src/__init__.py | 0 | ||||
| -rw-r--r-- | src/initial_data.json | 20 | ||||
| -rwxr-xr-x | src/manage.py | 11 | ||||
| -rw-r--r-- | src/rest/__init__.py | 0 | ||||
| -rw-r--r-- | src/rest/emitters.py | 39 | ||||
| -rw-r--r-- | src/rest/parsers.py | 18 | ||||
| -rw-r--r-- | src/rest/resource.py | 113 | ||||
| -rw-r--r-- | src/settings.py | 97 | ||||
| -rw-r--r-- | src/testarchive/__init__.py | 0 | ||||
| -rw-r--r-- | src/testarchive/models.py | 3 | ||||
| -rw-r--r-- | src/testarchive/templates/emitter.html | 11 | ||||
| -rw-r--r-- | src/testarchive/templates/emitter.txt | 4 | ||||
| -rw-r--r-- | src/testarchive/templates/emitter.xhtml | 3 | ||||
| -rw-r--r-- | src/testarchive/tests.py | 23 | ||||
| -rw-r--r-- | src/testarchive/urls.py | 7 | ||||
| -rw-r--r-- | src/testarchive/views.py | 8 | ||||
| -rw-r--r-- | src/urls.py | 15 | 
19 files changed, 380 insertions, 0 deletions
| diff --git a/.hgignore b/.hgignore new file mode 100644 index 00000000..f72e6586 --- /dev/null +++ b/.hgignore @@ -0,0 +1,5 @@ +syntax: glob + +*.pyc +*.db +env diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..8144d828 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Django==1.2.4 +distribute==0.6.14 +wsgiref==0.1.2 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/__init__.py diff --git a/src/initial_data.json b/src/initial_data.json new file mode 100644 index 00000000..62103cf9 --- /dev/null +++ b/src/initial_data.json @@ -0,0 +1,20 @@ +[ +    { +        "pk": 1,  +        "model": "auth.user",  +        "fields": { +            "username": "admin",  +            "first_name": "",  +            "last_name": "",  +            "is_active": true,  +            "is_superuser": true,  +            "is_staff": true,  +            "last_login": "2010-01-01 00:00:00",  +            "groups": [],  +            "user_permissions": [],  +            "password": "sha1$6cbce$e4e808893d586a3301ac3c14da6c84855999f1d8",  +            "email": "test@example.com",  +            "date_joined": "2010-01-01 00:00:00" +        } +    } +]
\ No newline at end of file diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 00000000..5e78ea97 --- /dev/null +++ b/src/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: +    import settings # Assumed to be in the same directory. +except ImportError: +    import sys +    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) +    sys.exit(1) + +if __name__ == "__main__": +    execute_manager(settings) diff --git a/src/rest/__init__.py b/src/rest/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/rest/__init__.py diff --git a/src/rest/emitters.py b/src/rest/emitters.py new file mode 100644 index 00000000..fa6a2ffd --- /dev/null +++ b/src/rest/emitters.py @@ -0,0 +1,39 @@ +from django.template import Context, loader +import json + +class BaseEmitter(object): +    def __init__(self, resource, status, headers): +        self.resource = resource +        self.status = status +        self.headers = headers +     +    def emit(self, output): +        return output + +class TemplatedEmitter(BaseEmitter): +    def emit(self, output): +        content = json.dumps(output, indent=4) +        template = loader.get_template(self.template) +        context = Context({ +            'content': content, +            'status': self.status, +            'headers': self.headers, +            'resource_name': self.resource.__class__.__name__, +            'resource_doc': self.resource.__doc__ +        }) +        return template.render(context) +     +class JSONEmitter(BaseEmitter): +    def emit(self, output): +        return json.dumps(output) + +class XMLEmitter(BaseEmitter): +    pass + +class HTMLEmitter(TemplatedEmitter): +    template = 'emitter.html' + +class TextEmitter(TemplatedEmitter): +    template = 'emitter.txt' + + diff --git a/src/rest/parsers.py b/src/rest/parsers.py new file mode 100644 index 00000000..038065f0 --- /dev/null +++ b/src/rest/parsers.py @@ -0,0 +1,18 @@ +class BaseParser(object): +    def __init__(self, resource, request): +        self.resource = resource +        self.request = request +     +    def parse(self, input): +        return {} + + +class JSONParser(BaseParser): +    pass + +class XMLParser(BaseParser): +    pass + +class FormParser(BaseParser): +    pass + diff --git a/src/rest/resource.py b/src/rest/resource.py new file mode 100644 index 00000000..7e4a6cd5 --- /dev/null +++ b/src/rest/resource.py @@ -0,0 +1,113 @@ +from django.http import HttpResponse +from decimal import Decimal +from rest import emitters, parsers + +class Resource(object): + +    callmap = { 'GET': 'read', 'POST': 'create',  +                'PUT': 'update', 'DELETE': 'delete' } + +    emitters = [ ('application/json', emitters.JSONEmitter), +                 ('text/html', emitters.HTMLEmitter), +                 ('application/xhtml+xml', emitters.HTMLEmitter), +                 ('text/plain', emitters.TextEmitter), +                 ('application/xml', emitters.XMLEmitter), ] + +    parsers = { 'application/json': parsers.JSONParser, +                'application/xml': parsers.XMLParser, +                'application/x-www-form-urlencoded': parsers.FormParser } + +    def __new__(cls, request, *args, **kwargs): +        self = object.__new__(cls) +        self.__init__() +        return self._handle_request(request, *args, **kwargs) + +    def __init__(self): +        pass + +    def _determine_parser(self, request): +        """Return the appropriate parser for the input, given the client's 'Content-Type' header, +        and the content types that this Resource knows how to parse.""" +        print request.META +        return self.parsers.values()[0] +     +        # TODO: Raise 415 Unsupported media type +     +    def _determine_emitter(self, request): +        """Return the appropriate emitter for the output, given the client's 'Accept' header, +        and the content types that this Resource knows how to serve. +         +        See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html +        """ + +        default = self.emitters[0] + +        if not request.META.has_key('HTTP_ACCEPT'): +            return default +         +        # Parse the accept header into a dict of {Priority: List of Mimetypes} +        accept_list = [item.split(';') for item in request.META["HTTP_ACCEPT"].split(',')] +        accept_dict = {}     +        for item in accept_list: +            mimetype = item[0].strip() +            qvalue = Decimal('1.0') +             +            if len(item) > 1: +                # Parse items that have a qvalue eg text/html;q=0.9 +                try: +                    (q, num) = item[1].split('=') +                    if q == 'q': +                        qvalue = Decimal(num) +                except: +                    # Skip malformed entries +                    continue + +            if accept_dict.has_key(qvalue): +                accept_dict[qvalue].append(mimetype) +            else: +                accept_dict[qvalue] = [mimetype] +         +        # Go through all accepted mimetypes in priority order and return our first match +        qvalues = accept_dict.keys() +        qvalues.sort(reverse=True) +        +        for qvalue in qvalues: +            for (mimetype, emitter) in self.emitters: +                for accept_mimetype in accept_dict[qvalue]: +                    if ((accept_mimetype == '*/*') or +                        (accept_mimetype.endswith('/*') and mimetype.startswith(accept_mimetype[:-1])) or +                        (accept_mimetype == mimetype)): +                            return (mimetype, emitter)       + +        # TODO: Raise 406, Not Acceptable + +    def _handle_request(self, request, *args, **kwargs): +        meth = request.method +         +        # Parse the HTTP Request content +        if meth in ('PUT', 'POST'): +            parser = self._determine_parser(request) +            data = parser(self, request).parse(request.raw_post_data) + +        if meth == "POST": +            (status, ret, headers) = self.handle_post(data, request.META, *args, **kwargs) +        else: +            (status, ret, headers) = self.handle_get(request.META, *args, **kwargs) + +        # Serialize the HTTP Response content +        mimetype, emitter = self._determine_emitter(request) +        content = emitter(self, status, headers).emit(ret) +        print mimetype, emitter, content + +        # Build the HTTP Response +        resp = HttpResponse(content, mimetype=mimetype, status=status) +        for (key, val) in headers.items(): +            resp[key] = val + +        return resp + +    def handle_get(self): +        raise NotImplementedError(self.handle_get) + +    def handle_post(self): +        raise NotImplementedError(self.handle_post)
\ No newline at end of file diff --git a/src/settings.py b/src/settings.py new file mode 100644 index 00000000..2a55c76a --- /dev/null +++ b/src/settings.py @@ -0,0 +1,97 @@ +# Django settings for src project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( +    # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASES = { +    'default': { +        'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +        'NAME': 'sqlite3.db',                   # Or path to database file if using sqlite3. +        'USER': '',                      # Not used with sqlite3. +        'PASSWORD': '',                  # Not used with sqlite3. +        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3. +        'PORT': '',                      # Set to empty string for default. Not used with sqlite3. +    } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'Europe/London' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-uk' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 't&9mru2_k$t8e2-9uq-wu2a1)9v*us&j3i#lsqkt(lbx*vh1cu' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( +    'django.template.loaders.filesystem.Loader', +    'django.template.loaders.app_directories.Loader', +#     'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( +    'django.middleware.common.CommonMiddleware', +    'django.contrib.sessions.middleware.SessionMiddleware', +    'django.middleware.csrf.CsrfViewMiddleware', +    'django.contrib.auth.middleware.AuthenticationMiddleware', +    'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'src.urls' + +TEMPLATE_DIRS = ( +    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". +    # Always use forward slashes, even on Windows. +    # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( +    'django.contrib.auth', +    'django.contrib.contenttypes', +    'django.contrib.sessions', +    'django.contrib.sites', +    'django.contrib.messages', +    # Uncomment the next line to enable the admin: +    'django.contrib.admin', +    # Uncomment the next line to enable admin documentation: +    # 'django.contrib.admindocs', +    'testarchive', +) diff --git a/src/testarchive/__init__.py b/src/testarchive/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/testarchive/__init__.py diff --git a/src/testarchive/models.py b/src/testarchive/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/src/testarchive/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/src/testarchive/templates/emitter.html b/src/testarchive/templates/emitter.html new file mode 100644 index 00000000..4c843aa3 --- /dev/null +++ b/src/testarchive/templates/emitter.html @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"  +        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +  <body> +    <h1>{{ resource_name }}</h1> +    <p>{{ resource_doc }}</p> +    <pre> +{% include 'emitter.txt' %}    </pre> +  </body> +</html>
\ No newline at end of file diff --git a/src/testarchive/templates/emitter.txt b/src/testarchive/templates/emitter.txt new file mode 100644 index 00000000..3bf094c6 --- /dev/null +++ b/src/testarchive/templates/emitter.txt @@ -0,0 +1,4 @@ +{% autoescape off %}HTTP Status {{ status }} +{% for key, val in headers.items %}{{ key }}: {{ val }} +{% endfor %} +{{ content }}{% endautoescape %}
\ No newline at end of file diff --git a/src/testarchive/templates/emitter.xhtml b/src/testarchive/templates/emitter.xhtml new file mode 100644 index 00000000..d9fb3ce9 --- /dev/null +++ b/src/testarchive/templates/emitter.xhtml @@ -0,0 +1,3 @@ +HTML: + +{{ content }}
\ No newline at end of file diff --git a/src/testarchive/tests.py b/src/testarchive/tests.py new file mode 100644 index 00000000..2aaf1bab --- /dev/null +++ b/src/testarchive/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): +    def test_basic_addition(self): +        """ +        Tests that 1 + 1 always equals 2. +        """ +        self.failUnlessEqual(1 + 1, 3) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/src/testarchive/urls.py b/src/testarchive/urls.py new file mode 100644 index 00000000..81aa0fa8 --- /dev/null +++ b/src/testarchive/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import patterns +from testarchive.views import RootResource + + +urlpatterns = patterns('', +    (r'^$', RootResource), +) diff --git a/src/testarchive/views.py b/src/testarchive/views.py new file mode 100644 index 00000000..a7812dc3 --- /dev/null +++ b/src/testarchive/views.py @@ -0,0 +1,8 @@ +from rest.resource import Resource + +class RootResource(Resource): +    """This is my docstring +    """ + +    def handle_get(self, headers={}, *args, **kwargs): +        return (200, {'Name': 'Test', 'Value': 1}, {'Location': 'BLAH'}) diff --git a/src/urls.py b/src/urls.py new file mode 100644 index 00000000..f6c598ce --- /dev/null +++ b/src/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls.defaults import patterns, include +from django.contrib import admin + +admin.autodiscover() + +urlpatterns = patterns('', +    # Example: +    (r'^testarchive/', include('testarchive.urls')), + +    # Uncomment the admin/doc line below to enable admin documentation: +    (r'^admin/doc/', include('django.contrib.admindocs.urls')), + +    # Uncomment the next line to enable the admin: +    (r'^admin/', include(admin.site.urls)), +) | 
