Commits

Anonymous committed 658570e

Added kay.generics.rest module for rest operations.

Comments (0)

Files changed (9)

kay/generics/__init__.py

 :Copyright: (c) 2009 Takashi Matsuo <tmatsuo@candit.jp> All rights reserved.
 :license: BSD, see LICENSE for more details.
 """
+
+OP_LIST = 'list'
+OP_SHOW = 'show'
+OP_CREATE = 'create'
+OP_UPDATE = 'update'
+OP_DELETE = 'delete'

kay/generics/crud.py

 from kay.i18n import lazy_gettext
 from kay.routing import ViewGroup
 
+from kay.generics import (
+  OP_LIST, OP_SHOW, OP_CREATE, OP_UPDATE, OP_DELETE
+)
+
 endpoints = {
   'list': "list_$model",
   'show': "show_$model",
   'delete': "a/delete_$model",
 }
 
-OP_LIST = 'list'
-OP_SHOW = 'show'
-OP_CREATE = 'create'
-OP_UPDATE = 'update'
-OP_DELETE = 'delete'
-
-
 # presets for authorization
 
 def login_required(self, request, operation, obj=None):

kay/generics/rest.py

+# -*- coding: utf-8 -*-
+
+"""
+Kay generics rest.
+
+# Copyright (c) 2008 Boomi, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+This file is originally developped in appengine-rest-server project
+hosted at:
+http://code.google.com/p/appengine-rest-server
+
+Additional part (Adaption to Kay, parsing json data, etc..) is written
+by Takashi Matsuo <tmatsuo@candit.jp>
+
+:Copyright: (c) 2009 Takashi Matsuo <tmatsuo@candit.jp> All rights reserved.
+
+"""
+
+import logging
+import types
+import logging
+import re
+import base64
+from datetime import datetime
+from xml.dom import minidom
+
+import simplejson
+from google.appengine.ext import db
+from google.appengine.api import memcache
+
+from werkzeug.routing import (
+    Rule, RuleTemplate, EndpointPrefix, Submount,
+)
+from werkzeug.exceptions import (
+    NotFound, Forbidden, BadRequest, InternalServerError
+)
+from werkzeug import (
+    Response, redirect
+)
+from werkzeug.utils import import_string
+from werkzeug.routing import RequestRedirect
+
+from kay.utils import (
+    render_to_response, url_for
+)
+from kay.db import OwnerProperty
+from kay.exceptions import NotAuthorized
+from kay.i18n import gettext as _
+from kay.i18n import lazy_gettext
+from kay.routing import ViewGroup
+
+
+def get_instance_type_name(value):
+    """Returns the name of the type of the given instance."""
+    return get_type_name(type(value))
+
+def get_type_name(value_type):
+    """Returns the name of the given type."""
+    return value_type.__name__
+    
+METADATA_PATH = "metadata"
+
+MAX_FETCH_PAGE_SIZE = 1000
+
+XML_CLEANSE_PATTERN1 = re.compile(r"^(\d)")
+XML_CLEANSE_REPL1 = r"_\1"
+XML_CLEANSE_PATTERN2 = re.compile(r"[^a-zA-Z0-9]")
+XML_CLEANSE_REPL2 = r"_"
+
+EMPTY_VALUE = object()
+MULTI_UPDATE_KEY = object()
+
+KEY_PROPERTY_NAME = "key"
+KEY_PROPERTY_TYPE = "KeyProperty"
+KEY_QUERY_FIELD = "__key__"
+
+TYPES_EL_NAME = "types"
+TYPE_EL_NAME = "type"
+LIST_EL_NAME = "list"
+TYPE_ATTR_NAME = "type"
+NAME_ATTR_NAME = "name"
+ITEM_EL_NAME = "item"
+
+DATA_TYPE_SEPARATOR = ":"
+
+DATE_TIME_SEP = "T"
+DATE_FORMAT = "%Y-%m-%d"
+TIME_FORMAT_NO_MS = "%H:%M:%S"
+DATE_TIME_FORMAT_NO_MS = DATE_FORMAT + DATE_TIME_SEP + TIME_FORMAT_NO_MS
+
+TRUE_VALUE = "true"
+TRUE_NUMERIC_VALUE = "1"
+
+CONTENT_TYPE_HEADER = "Content-Type"
+XML_CONTENT_TYPE = "application/xml"
+TEXT_CONTENT_TYPE = "text/plain"
+JSON_CONTENT_TYPE = "application/json"
+METHOD_OVERRIDE_HEADER = "X-HTTP-Method-Override"
+
+XML_ENCODING = "utf-8"
+XSD_PREFIX = "xs"
+XSD_ATTR_XMLNS = "xmlns:" + XSD_PREFIX
+XSD_NS = "http://www.w3.org/2001/XMLSchema"
+XSD_SCHEMA_NAME = XSD_PREFIX + ":schema"
+XSD_ELEMENT_NAME = XSD_PREFIX + ":element"
+XSD_COMPLEXTYPE_NAME = XSD_PREFIX + ":complexType"
+XSD_SEQUENCE_NAME = XSD_PREFIX + ":sequence"
+XSD_ANY_NAME = XSD_PREFIX + ":any"
+XSD_ANNOTATION_NAME = XSD_PREFIX + ":annotation"
+XSD_APPINFO_NAME = XSD_PREFIX + ":appinfo"
+XSD_FILTER_PREFIX = "bm"
+XSD_ATTR_FILTER_XMLNS = "xmlns:" + XSD_FILTER_PREFIX
+XSD_FILTER_NS = "http://www.boomi.com/connector/annotation"
+XSD_FILTER_NAME = XSD_FILTER_PREFIX + ":filter"
+XSD_ATTR_MINOCCURS = "minOccurs"
+XSD_ATTR_MAXOCCURS = "maxOccurs"
+XSD_ATTR_NAMESPACE = "namespace"
+XSD_ATTR_PROCESSCONTENTS = "processContents"
+XSD_ATTR_NOFILTER = "ignore"
+XSD_ANY_NAMESPACE = "##any"
+XSD_LAX_CONTENTS = "lax"
+XSD_NO_MIN = "0"
+XSD_SINGLE_MAX = "1"
+XSD_NO_MAX = "unbounded"
+
+QUERY_OFFSET_PARAM = "offset"
+QUERY_PAGE_SIZE_PARAM = "page_size"
+QUERY_ORDERING_PARAM = "ordering"
+QUERY_TERM_PATTERN = re.compile(r"^(f.._)(.+)$")
+QUERY_PREFIX = "WHERE "
+QUERY_JOIN = " AND "
+QUERY_ORDERBY = " ORDER BY "
+QUERY_ORDER_ASC = " ASC"
+QUERY_ORDER_DESC = " DESC"
+QUERY_LIST_TYPE = "fin_"
+
+QUERY_EXPRS = {
+    "feq_" : "%s = :%d",
+    "flt_" : "%s < :%d",
+    "fgt_" : "%s > :%d",
+    "fle_" : "%s <= :%d",
+    "fge_" : "%s >= :%d",
+    "fne_" : "%s != :%d",
+    QUERY_LIST_TYPE : "%s IN :%d"
+    }
+
+DATA_TYPE_TO_PROPERTY_TYPE = {
+    "basestring" : db.StringProperty,
+    "str" : db.StringProperty,
+    "unicode" : db.StringProperty,
+    "bool" : db.BooleanProperty,
+    "int" : db.IntegerProperty,
+    "long" : db.IntegerProperty,
+    "float" : db.FloatProperty,
+    "Key" : db.ReferenceProperty,
+    "datetime" : db.DateTimeProperty,
+    "date" : db.DateProperty,
+    "time" : db.TimeProperty,
+    "Blob" : db.BlobProperty,
+    "Text" : db.TextProperty,
+    "User" : db.UserProperty,
+    "Category" : db.CategoryProperty,
+    "Link" : db.LinkProperty,
+    "Email" : db.EmailProperty,
+    "GeoPt" : db.GeoPtProperty,
+    "IM" : db.IMProperty,
+    "PhoneNumber" : db.PhoneNumberProperty,
+    "PostalAddress" : db.PostalAddressProperty,
+    "Rating" : db.RatingProperty,
+    "list" : db.ListProperty,
+    "tuple" : db.ListProperty
+    }
+
+PROPERTY_TYPE_TO_XSD_TYPE = {
+    get_type_name(db.StringProperty) : XSD_PREFIX + ":string",
+    get_type_name(db.BooleanProperty) : XSD_PREFIX + ":boolean",
+    get_type_name(db.IntegerProperty) : XSD_PREFIX + ":long",
+    get_type_name(db.FloatProperty) : XSD_PREFIX + ":double",
+    get_type_name(db.ReferenceProperty) : XSD_PREFIX + ":normalizedString",
+    get_type_name(db.DateTimeProperty) : XSD_PREFIX + ":dateTime",
+    get_type_name(db.DateProperty) : XSD_PREFIX + ":date",
+    get_type_name(db.TimeProperty) : XSD_PREFIX + ":time",
+    get_type_name(db.BlobProperty) : XSD_PREFIX + ":base64Binary",
+    get_type_name(db.TextProperty) : XSD_PREFIX + ":string",
+    get_type_name(db.UserProperty) : XSD_PREFIX + ":normalizedString",
+    get_type_name(db.CategoryProperty) : XSD_PREFIX + ":normalizedString",
+    get_type_name(db.LinkProperty) : XSD_PREFIX + ":anyURI",
+    get_type_name(db.EmailProperty) : XSD_PREFIX + ":normalizedString",
+    get_type_name(db.GeoPtProperty) : XSD_PREFIX + ":normalizedString",
+    get_type_name(db.IMProperty) : XSD_PREFIX + ":normalizedString",
+    get_type_name(db.PhoneNumberProperty) : XSD_PREFIX + ":normalizedString",
+    get_type_name(db.PostalAddressProperty) : XSD_PREFIX + ":normalizedString",
+    get_type_name(db.RatingProperty) : XSD_PREFIX + ":integer",
+    KEY_PROPERTY_TYPE : XSD_PREFIX + ":normalizedString"
+    }
+
+def parse_date_time(dt_str, dt_format, dt_type, allows_microseconds):
+    """Returns a datetime/date/time instance parsed from the given string using the given format info."""
+    ms = None
+    if(allows_microseconds):
+        dt_parts = dt_str.rsplit(".", 1)
+        dt_str = dt_parts.pop(0)
+        if(len(dt_parts) > 0):
+            ms = int(dt_parts[0].ljust(6,"0")[:6])
+    dt = datetime.strptime(dt_str, dt_format)
+    if(ms):
+        dt = dt.replace(microsecond=ms)
+    if(dt_type is datetime.date):
+        dt = dt.date()
+    elif(dt_type is datetime.time):
+        dt = dt.time()
+    return dt
+    
+def convert_to_valid_xml_name(name):
+    """Converts a string to a valid xml element name."""
+    name = re.sub(XML_CLEANSE_PATTERN1, XML_CLEANSE_REPL1, name)
+    return re.sub(XML_CLEANSE_PATTERN2, XML_CLEANSE_REPL2, name)
+
+def append_child(parent_el, name, content=None):
+    """Returns a new xml element with the given name and optional text content appended to the given parent element."""
+    doc = parent_el.ownerDocument
+    el = doc.createElement(name)
+    parent_el.appendChild(el)
+    if content:
+        el.appendChild(doc.createTextNode(content))
+    return el
+
+def xsd_append_sequence(parent_el):
+    """Returns an XML Schema sub-sequence (complex type, then sequence) appended to the given parent element."""
+    ctype_el = append_child(parent_el, XSD_COMPLEXTYPE_NAME)
+    seq_el = append_child(ctype_el, XSD_SEQUENCE_NAME)
+    return seq_el
+
+def xsd_append_nofilter(parent_el):
+    """Returns Boomi XML Schema no filter annotation appended to the given parent element."""
+    child_el = append_child(parent_el, XSD_ANNOTATION_NAME)
+    child_el = append_child(child_el, XSD_APPINFO_NAME)
+    filter_el = append_child(child_el, XSD_FILTER_NAME)
+    filter_el.attributes[XSD_ATTR_FILTER_XMLNS] = XSD_FILTER_NS
+    filter_el.attributes[XSD_ATTR_NOFILTER] = TRUE_VALUE
+    return filter_el
+
+def xsd_append_element(parent_el, name, prop_type_name, min_occurs, max_occurs):
+    """Returns an XML Schema element with the given attributes appended to the given parent element."""
+    element_el = append_child(parent_el, XSD_ELEMENT_NAME)
+    element_el.attributes[NAME_ATTR_NAME] = name
+    type_name = PROPERTY_TYPE_TO_XSD_TYPE.get(prop_type_name, None)
+    if type_name:
+        element_el.attributes[TYPE_ATTR_NAME] = type_name
+    if min_occurs is not None:
+        element_el.attributes[XSD_ATTR_MINOCCURS] = min_occurs
+    if max_occurs is not None:
+        element_el.attributes[XSD_ATTR_MAXOCCURS] = max_occurs
+    return element_el
+    
+def get_node_text(node_list, do_strip=False):
+    """Returns the complete text from the given node list (optionally stripped) or None if the list is empty."""
+    if(len(node_list) == 0):
+        return None
+    text = u""
+    for node in node_list:
+        if node.nodeType == node.TEXT_NODE:
+            text += node.data
+    if do_strip:
+        text = text.strip()
+    return text
+
+def xml_to_json(xml_doc):
+    doc_el = xml_doc.documentElement
+    json_doc = {doc_el.nodeName : xml_node_to_json(doc_el)}
+
+    return simplejson.dumps(json_doc)
+
+def xml_node_to_json(xml_node):
+    if((len(xml_node.childNodes) == 1) and
+       (xml_node.childNodes[0].nodeType == xml_node.TEXT_NODE)):
+        return xml_node.childNodes[0].data
+    else:
+        json_node = {}
+        
+        for child_xml_node in xml_node.childNodes:
+            new_child_json_node = xml_node_to_json(child_xml_node)
+            cur_child_json_node = json_node.get(child_xml_node.nodeName, None)
+            if(cur_child_json_node is None):
+                cur_child_json_node = new_child_json_node
+            else:
+                # if we have more than one of the same type, turn the children into a list
+                if(not isinstance(cur_child_json_node, types.ListType)):
+                    cur_child_json_node = [cur_child_json_node]
+                cur_child_json_node.append(new_child_json_node)
+            json_node[child_xml_node.nodeName] = cur_child_json_node
+            
+        xml_node_attrs = xml_node.attributes
+        for attr_name in xml_node_attrs.keys():
+            json_node["@" + attr_name] = xml_node_attrs[attr_name].nodeValue
+
+        return json_node
+
+class PropertyHandler(object):
+    """Base handler for Model properties which manages converting properties to and from xml.
+
+    This implementation works for most properties which have simple to/from string conversions.
+    
+    """
+    
+    def __init__(self, property_name, property_type):
+        self.property_name = property_name
+        self.property_type = property_type
+
+        # most types can be parsed from stripped strings, but don't strip text data
+        self.strip_on_read = True
+        if(isinstance(property_type, (db.StringProperty, db.TextProperty))):
+            self.strip_on_read = False
+
+    def get_query_field(self):
+        """Returns the field name which should be used to query this property."""
+        return self.property_name
+            
+    def can_query(self):
+        """Returns True if this property can be used as a query filter, False otherwise."""
+        return True
+            
+    def get_data_type(self):
+        """Returns the type of data this property accepts."""
+        return self.property_type.data_type
+            
+    def empty(self, value):
+        """Tests a property value for empty in a manner specific to the property type."""
+        return self.property_type.empty(value)
+
+    def get_type_string(self):
+        """Returns the type string describing this property."""
+        return get_instance_type_name(self.property_type)
+
+    def get_value(self, model):
+        """Returns the value for this property from the given model instance."""
+        return getattr(model, self.property_name)
+
+    def get_value_as_string(self, model):
+        """Returns the value for this property from the given model instance as a string (or EMPTY_VALUE if the value
+        is empty as defined by this property type), used by the default write_xml_value() method."""
+        value = self.get_value(model)
+        if(self.empty(value)):
+            return EMPTY_VALUE
+        return self.value_to_string(value)
+
+    def value_to_string(self, value):
+        """Returns the given property value as a string, used by the default get_value_as_string() method."""
+        return unicode(value)
+
+    def value_from_string(self, value):
+        """Returns the value for this property from the given string value (may be None), used by the default
+        read_xml_value() method."""
+        if((value is None) or (self.strip_on_read and not value)):
+            return None
+        if(not isinstance(value, self.get_data_type())):
+            value = self.get_data_type()(value)
+        return value
+
+    def value_for_query(self, value):
+        """Returns the value for this property from the given string value (may be None), for use in a query filter."""
+        return self.value_from_string(value)
+                
+    def write_xml_value(self, parent_el, prop_xml_name, model):
+        """Returns the property value from the given model instance converted to an xml element and appended to the
+        given parent element."""
+        value = self.get_value_as_string(model)
+        if(value is EMPTY_VALUE):
+            return None
+        return append_child(parent_el, prop_xml_name, value)
+
+    def read_xml_value(self, props, prop_el):
+        """Adds the value for this property to the given property dict converted from an xml element."""
+        value = self.value_from_string(get_node_text(prop_el.childNodes, self.strip_on_read))
+        props[self.property_name] = value
+
+    def write_xsd_metadata(self, parent_el, prop_xml_name):
+        """Returns the XML Schema element for this property type appended to the given parent element."""
+        prop_el = xsd_append_element(parent_el, prop_xml_name, self.get_type_string(), XSD_NO_MIN, XSD_SINGLE_MAX)
+        if(not self.can_query()):
+            xsd_append_nofilter(prop_el)
+        return prop_el
+    
+        
+class DateTimeHandler(PropertyHandler):
+    """PropertyHandler for datetime/data/time property instances."""
+    
+    def __init__(self, property_name, property_type):
+        super(DateTimeHandler, self).__init__(property_name, property_type)
+
+        self.format_args = []
+        if(isinstance(property_type, db.DateProperty)):
+            self.dt_format = DATE_FORMAT
+            self.dt_type = datetime.date
+            self.allows_microseconds = False
+        elif(isinstance(property_type, db.TimeProperty)):
+            self.dt_format = TIME_FORMAT_NO_MS
+            self.dt_type = datetime.time
+            self.allows_microseconds = True
+        elif(isinstance(property_type, db.DateTimeProperty)):
+            self.dt_format = DATE_TIME_FORMAT_NO_MS
+            self.dt_type = datetime
+            self.allows_microseconds = True
+            self.format_args.append(DATE_TIME_SEP)
+        else:
+            raise ValueError("unexpected property type %s for DateTimeHandler" % property_type)
+
+    def value_to_string(self, value):
+        """Returns the datetime/date/time value converted to the relevant iso string value."""
+        value_str = value.isoformat(*self.format_args)
+        # undo python's idiotic formatting irregularity
+        if(self.allows_microseconds and not value.microsecond):
+            value_str += ".000000"
+        return unicode(value_str)
+
+    def value_from_string(self, value):
+        """Returns the datetime/date/time parsed from the relevant iso string value, or None if the string is empty."""
+        if(not value):
+            return None
+        return parse_date_time(value, self.dt_format, self.dt_type, self.allows_microseconds)
+
+    
+class BooleanHandler(PropertyHandler):
+    """PropertyHandler for boolean property instances."""
+    
+    def __init__(self, property_name, property_type):
+        super(BooleanHandler, self).__init__(property_name, property_type)
+
+    def value_to_string(self, value):
+        """Returns the boolean value converted to a string value of 'true' or 'false'."""
+        return unicode(value).lower()
+
+    def value_from_string(self, value):
+        """Returns the boolean value parsed from the given string value: True for the strings 'true' (any case) and
+        '1', False for all other non-empty strings, and None otherwise."""
+        if(not value):
+            return None
+        value = value.lower()
+        return ((value == TRUE_VALUE) or (value == TRUE_NUMERIC_VALUE))
+
+    
+class TextHandler(PropertyHandler):
+    """PropertyHandler for (large) text property instances."""
+    
+    def __init__(self, property_name, property_type):
+        super(TextHandler, self).__init__(property_name, property_type)
+
+    def can_query(self):
+        """Text properties may not be used in query filters."""
+        return False
+
+
+class BlobHandler(PropertyHandler):
+    """PropertyHandler for blob property instances."""
+    
+    def __init__(self, property_name, property_type):
+        super(BlobHandler, self).__init__(property_name, property_type)
+
+    def can_query(self):
+        """Blob properties may not be used in query filters."""
+        return False
+            
+    def value_to_string(self, value):
+        """Returns a blob value converted to a Base64 encoded string."""
+        return base64.b64encode(str(value))
+
+    def value_from_string(self, value):
+        """Returns a blob value parsed from a Base64 encoded string, or None if the string is empty."""
+        if(not value):
+            return None
+        return base64.b64decode(value)
+
+
+class ReferenceHandler(PropertyHandler):
+    """PropertyHandler for reference property instances."""
+    
+    def __init__(self, property_name, property_type):
+        super(ReferenceHandler, self).__init__(property_name, property_type)
+
+    def get_data_type(self):
+        """Returns the db.Key type."""
+        return db.Key
+            
+    def get_value(self, model):
+        """Returns the key of the referenced model instance."""
+        return self.property_type.get_value_for_datastore(model)
+
+    
+class KeyHandler(ReferenceHandler):
+    """PropertyHandler for primary 'key' of a Model instance."""
+    
+    def __init__(self):
+        super(KeyHandler, self).__init__(KEY_PROPERTY_NAME, None)
+
+    def empty(self, value):
+        """Returns True if the value is any value which evaluates to False, False otherwise."""
+        return not value
+
+    def get_query_field(self):
+        """Returns the special key query field name '__key__'"""
+        return KEY_QUERY_FIELD
+                
+    def get_value(self, model):
+        """Returns the key of the given model instance if it has been saved, EMPTY_VALUE otherwise."""
+        if(not model.is_saved()):
+            return EMPTY_VALUE
+        return model.key()
+
+    def get_type_string(self):
+        """Returns the custom 'KeyProperty' type name."""
+        return KEY_PROPERTY_TYPE
+
+    
+class ListHandler(PropertyHandler):
+    """PropertyHandler for lists property instances."""
+    
+    def __init__(self, property_name, property_type):
+        super(ListHandler, self).__init__(property_name, property_type)
+        self.sub_handler = get_property_handler(ITEM_EL_NAME,
+                                                DATA_TYPE_TO_PROPERTY_TYPE[get_type_name(property_type.item_type)]())
+
+    def get_type_string(self):
+        """Returns the type string 'ListProperty:' + <sub_type_string>."""
+        return super(ListHandler, self).get_type_string() + DATA_TYPE_SEPARATOR + self.sub_handler.get_type_string()
+
+    def can_query(self):
+        """Can query is based on the list element type."""
+        return self.sub_handler.can_query()
+            
+    def value_for_query(self, value):
+        """Returns the value for a query filter based on the list element type."""
+        return self.sub_handler.value_from_string(value)
+                
+    def write_xml_value(self, parent_el, prop_xml_name, model):
+        """Returns a list element containing value elements for the property from the given model instance appended to
+        the given parent element."""
+        values = self.get_value(model)
+        if(not values):
+            return None
+        list_el = append_child(parent_el, prop_xml_name)
+        for value in values:
+            append_child(list_el, ITEM_EL_NAME, self.sub_handler.value_to_string(value))
+        return list_el
+
+    def read_xml_value(self, props, prop_el):
+        """Adds a list containing the property values to the given property dict converted from an xml list element."""
+        values = []
+        sub_props = {}
+        for item_node in prop_el.childNodes:
+            if((item_node.nodeType == item_node.ELEMENT_NODE) and (str(item_node.nodeName) == ITEM_EL_NAME)):
+                self.sub_handler.read_xml_value(sub_props, item_node)
+                values.append(sub_props.pop(ITEM_EL_NAME))
+        props[self.property_name] = values
+
+    def write_xsd_metadata(self, parent_el, prop_xml_name):
+        """Returns the XML Schema list element for this property type appended to the given parent element."""
+        list_el = super(ListHandler, self).write_xsd_metadata(parent_el, prop_xml_name)
+        seq_el = xsd_append_sequence(list_el)
+        xsd_append_element(seq_el, ITEM_EL_NAME, self.sub_handler.get_type_string(), XSD_NO_MIN, XSD_NO_MIN)
+        return list_el
+    
+class DynamicPropertyHandler(object):
+    """PropertyHandler for dynamic properties on Expando models."""
+    
+    def __init__(self, property_name):
+        self.property_name = property_name
+
+    def write_xml_value(self, parent_el, prop_xml_name, model):
+        """Returns the property value from the given model instance converted to an xml element (with a type
+        attribute) of the appropriate type and appended to the given parent element."""
+        value = getattr(model, self.property_name)
+        prop_handler = self.get_handler(None, value)
+        prop_el = prop_handler.write_xml_value(parent_el, prop_xml_name, model)
+        if prop_el:
+            prop_el.attributes[TYPE_ATTR_NAME] = prop_handler.get_type_string()
+        return prop_el
+
+    def read_xml_value(self, props, prop_el):
+        """Adds the value for this property to the given property dict converted from an xml element, either as a
+        StringProperty value if no type attribute exists or as the type given in a type attribute."""
+        attrs = prop_el.attributes
+        
+        if(attrs.has_key(TYPE_ATTR_NAME)):
+            prop_type = str(attrs[TYPE_ATTR_NAME].value)
+            self.get_handler(prop_type, None).read_xml_value(props, prop_el)
+            return
+
+        props[self.property_name] = get_node_text(prop_el.childNodes)
+
+    def get_handler(self, property_type, value):
+        """Returns the relevant PropertyHandler based on the given property_type string or property value."""
+        prop_args = []
+        sub_handler = None
+        if(value is not None):
+            property_type = DATA_TYPE_TO_PROPERTY_TYPE[get_instance_type_name(value)]
+            if(property_type is db.ListProperty):
+                prop_args.append(type(value[0]))
+
+        if(isinstance(property_type, basestring)):
+            if DATA_TYPE_SEPARATOR in property_type:
+                property_type, sub_property_type = property_type.split(DATA_TYPE_SEPARATOR, 1)
+                sub_handler = get_property_handler(ITEM_EL_NAME, getattr(db, sub_property_type)())
+                prop_args.append(sub_handler.get_data_type())
+            property_type = getattr(db, property_type)
+
+        property_type = property_type(*prop_args)
+
+        return get_property_handler(self.property_name, property_type)
+
+
+def get_property_handler(property_name, property_type):
+    """Returns a PropertyHandler instance with the given name appropriate for the given type."""
+    if(isinstance(property_type, (db.DateTimeProperty, db.TimeProperty, db.DateProperty))):
+        return DateTimeHandler(property_name, property_type)
+    elif(isinstance(property_type, db.BooleanProperty)):
+        return BooleanHandler(property_name, property_type)
+    elif(isinstance(property_type, db.ReferenceProperty)):
+        return ReferenceHandler(property_name, property_type)
+    elif(isinstance(property_type, db.BlobProperty)):
+        return BlobHandler(property_name, property_type)
+    elif(isinstance(property_type, db.TextProperty)):
+        return TextHandler(property_name, property_type)
+    elif(isinstance(property_type, db.ListProperty)):
+        return ListHandler(property_name, property_type)
+    
+    return PropertyHandler(property_name, property_type)
+
+
+class Lazy(object):
+    """Utility class for enabling lazy initialization of decorated properties."""
+    
+    def __init__(self, calculate_function):
+        self._calculate = calculate_function
+
+    def __get__(self, obj, _=None):
+        if obj is None:
+            return self
+        value = self._calculate(obj)
+        setattr(obj, self._calculate.func_name, value)
+        return value
+
+
+class ModelHandler(object):
+    """Handler for a Model (or Expando) type which manages converting instances to and from xml."""
+
+    def __init__(self, model_name, model_type):
+        self.model_name = model_name
+        self.model_type = model_type
+        self.key_handler = KeyHandler()
+
+    @Lazy
+    def property_handlers(self):
+        """Lazy initializer for the property_handlers dict."""
+        prop_handlers = {}
+        for prop_name, prop_type in self.model_type.properties().iteritems():
+            prop_handler = get_property_handler(prop_name, prop_type)
+            prop_handlers[convert_to_valid_xml_name(prop_handler.property_name)] = prop_handler
+        return prop_handlers
+            
+    def is_dynamic(self):
+        """Returns True if this Model type supports dynamic properties (is a subclass of Expando), False otherwise."""
+        return issubclass(self.model_type, db.Expando)
+
+    def get(self, key):
+        """Returns the model instance with the given key."""
+        return self.model_type.get(key)
+
+    def create(self, props):
+        """Returns a newly created model instance with the given properties (as a keyword dict)."""
+        return self.model_type(**props)
+
+    def get_all(self, limit, offset, ordering, query_expr, query_params):
+        """Returns all model instances of this type."""
+        if(query_expr is None):
+            query = self.model_type.all()
+            if(ordering):
+                query.order(ordering)
+        else:
+            if(ordering):
+                order_type = QUERY_ORDER_ASC
+                if(ordering[0] == "-"):
+                    ordering = ordering[1:]
+                    order_type = QUERY_ORDER_DESC
+                query_expr += QUERY_ORDERBY + ordering + order_type
+            query = self.model_type.gql(query_expr, *query_params)
+                
+        return query.fetch(limit, offset)
+    
+    def get_property_handler(self, prop_name):
+        """Returns the relevant property handler for the given property name."""
+        prop_name = str(prop_name)
+        if(prop_name == KEY_PROPERTY_NAME):
+            return self.key_handler
+        elif(self.property_handlers.has_key(prop_name)):
+            return self.property_handlers[prop_name]
+        elif(self.is_dynamic()):
+            return DynamicPropertyHandler(prop_name)
+        else:
+            raise KeyError("Unknown property %s" % prop_name)
+
+    def read_dict_value(self, model_dict):
+        """Returns a property dictionary for this Model from the given
+        model element."""
+        props = {}
+        for propname, propvalue in model_dict.values()[0].iteritems():
+            prop_handler = self.get_property_handler(propname)
+            props[str(propname)] = prop_handler.value_from_string(propvalue)
+        return props
+
+    def read_xml_value(self, model_el):
+        """Returns a property dictionary for this Model from the given model element."""
+        props = {}
+        for prop_node in model_el.childNodes:
+            if(prop_node.nodeType != prop_node.ELEMENT_NODE):
+                continue
+            self.get_property_handler(prop_node.nodeName).read_xml_value(props, prop_node)
+
+        return props
+
+    def read_xml_property(self, prop_el, props, prop_handler):
+        """Reads a property from a property element."""
+        prop_handler.read_xml_value(props, prop_el)
+
+    def read_query_values(self, prop_query_name, prop_query_values):
+        """Returns a tuple of (query_field, query_values) from the given query property name and value list."""
+        prop_handler = self.get_property_handler(prop_query_name)
+
+        if(not prop_handler.can_query()):
+            raise KeyError("Can not filter on property %s" % prop_query_name)
+
+        return (prop_handler.get_query_field(), [self.read_query_value(prop_handler, v) for v in prop_query_values])
+
+    def read_query_value(self, prop_handler, prop_query_value):
+        """Returns a query value from the given query property handler and value (may be a list)."""
+        if isinstance(prop_query_value, (types.ListType, types.TupleType)):
+            return [prop_handler.value_for_query(v) for v in prop_query_value]
+        else:
+            return prop_handler.value_for_query(prop_query_value)        
+        
+    def write_xml_value(self, model_el, model):
+        """Appends the properties of the given instance as xml elements to the given model element."""
+        # write key property first
+        self.write_xml_property(model_el, model, KEY_PROPERTY_NAME, self.key_handler)
+
+        # write static properties next
+        for prop_xml_name, prop_handler in self.property_handlers.iteritems():
+            self.write_xml_property(model_el, model, prop_xml_name, prop_handler)
+                
+        # write dynamic properties last
+        for prop_name in model.dynamic_properties():
+            prop_xml_name = convert_to_valid_xml_name(prop_name)
+            self.write_xml_property(model_el, model, prop_xml_name, DynamicPropertyHandler(prop_name))
+
+    def write_xml_property(self, model_el, model, prop_xml_name, prop_handler):
+        """Writes a property as a property element."""
+        prop_handler.write_xml_value(model_el, prop_xml_name, model)
+
+    def write_xsd_metadata(self, type_el, model_xml_name):
+        """Appends the XML Schema elements of the property types of this model type to the given parent element."""
+        top_el = append_child(type_el, XSD_ELEMENT_NAME)
+        top_el.attributes[NAME_ATTR_NAME] = model_xml_name
+        seq_el = xsd_append_sequence(top_el)
+
+        self.key_handler.write_xsd_metadata(seq_el, KEY_PROPERTY_NAME)
+        
+        for prop_xml_name, prop_handler in self.property_handlers.iteritems():
+            prop_handler.write_xsd_metadata(seq_el, prop_xml_name)
+
+        if(self.is_dynamic()):
+            any_el = append_child(seq_el, XSD_ANY_NAME)
+            any_el.attributes[XSD_ATTR_NAMESPACE] = XSD_ANY_NAMESPACE
+            any_el.attributes[XSD_ATTR_PROCESSCONTENTS] = XSD_LAX_CONTENTS
+            any_el.attributes[XSD_ATTR_MINOCCURS] = XSD_NO_MIN
+            any_el.attributes[XSD_ATTR_MAXOCCURS] = XSD_NO_MAX
+
+class RESTViewGroup(ViewGroup):
+    models = []
+    fetch_page_size = 50
+    rest_rule_template = RuleTemplate([
+        Rule("/$prefix/metadata", endpoint="$prefix/metadata"),
+        Rule("/$prefix/metadata/<model_name>", endpoint="$prefix/metadata"),
+        Rule("/$prefix/<model_name>", endpoint="$prefix/model"),
+        Rule("/$prefix/<model_name>/<key>", endpoint="$prefix/entity"),
+        Rule("/$prefix/<model_name>/<key>/<prop>", endpoint="$prefix/prop"),
+    ])
+
+    def __init__(self, models=None, name="rest", **kwargs):
+      super(RESTViewGroup, self).__init__(**kwargs)
+      self.name = name
+      self._models = models or self.models
+      self.models_dict = {}
+      self.model_handlers = {}
+      for model in self._models:
+          if isinstance(model, basestring):
+              model_name = model.split(".")[-1]
+          else:
+              model_name = model.__name__
+          xml_name = convert_to_valid_xml_name(model_name)
+          self.models_dict[xml_name] = model
+          self.model_handlers[xml_name] = None
+
+    def _import_model_if_not(self, model_name):
+        if isinstance(self.models_dict[model_name], basestring):
+            self.models_dict[model_name] = import_string(
+                self.models_dict[model_name])
+        if self.model_handlers[model_name] is None:
+            self.model_handlers[model_name] = ModelHandler(
+                model_name, self.models_dict[model_name])
+
+    def _get_rules(self):
+        return [self.rest_rule_template(prefix=self.name)]
+
+    def _get_views(self, prefix=None):
+        self.prefix = prefix
+        ret = {}
+        for endpoint in ["metadata", "model", "entity", "prop"]:
+            actual_endpoint = self.name + "/" + endpoint
+            if prefix:
+                actual_endpoint = prefix+actual_endpoint
+            ret[actual_endpoint] = getattr(self, endpoint)
+        return ret
+
+    def metadata(self, request, model_name=None):
+        impl = minidom.getDOMImplementation()
+        doc = None
+        try:
+            if model_name:
+                model_handler = self.get_model_handler(model_name)
+                if(not model_handler):
+                    return None
+                doc = impl.createDocument(XSD_NS, XSD_SCHEMA_NAME, None)
+                doc.documentElement.attributes[XSD_ATTR_XMLNS] = XSD_NS
+                model_handler.write_xsd_metadata(doc.documentElement,
+                                                 model_name)
+            else:
+                doc = impl.createDocument(None, TYPES_EL_NAME, None)
+                types_el = doc.documentElement
+                for model_name in self.model_handlers.iterkeys():
+                    append_child(types_el, TYPE_EL_NAME, model_name)
+            output = self.doc_to_output(request, doc)
+        finally:
+            if doc:
+                doc.unlink()
+        return self.out_to_response(output)
+
+    def prop(self, request, model_name=None, key=None, prop=None):
+        model_handler = self.get_model_handler(model_name)
+        model = model_handler.get(key)
+        prop_handler = model_handler.get_property_handler(prop)
+        prop_value = prop_handler.get_value(model)
+        content_type = request.accept_mimetypes.best
+        if not content_type:
+            content_type = TEXT_CONTENT_TYPE
+        return Response(str(prop_value), content_type=content_type)
+
+    def entity(self, request, model_name=None, key=None):
+        real_method = request.method
+        if request.method == "POST":
+            real_method = request.headers.get(METHOD_OVERRIDE_HEADER, None)
+            if real_method:
+                real_method = real_method.upper()
+            else:
+                real_method = "POST"
+
+        if real_method == "GET":
+            model_handler = self.get_model_handler(model_name)
+            model = model_handler.get(key)
+            if model is None:
+                return NotFound()
+            return self.out_to_response(
+                self.models_to_xml(request, model_name, model_handler, model,
+                                   {}))
+        elif real_method == "POST":
+            return self.update_impl(request, model_name, key, False)
+        elif real_method == "PUT":
+            return self.update_impl(request, model_name, key, True)
+        elif real_method == "DELETE":
+            model_handler = self.get_model_handler(model_name)
+            try:
+                db.delete(db.Key(key))
+                return Response("OK")
+            except Exception:
+                logging.warning("delete failed", exc_info=1)
+                return InternalServerError()
+      
+    def model(self, request, model_name=None):
+        real_method = request.method
+        if request.method == "POST":
+            real_method = request.headers.get(METHOD_OVERRIDE_HEADER, None)
+            if real_method:
+                real_method = real_method.upper()
+            else:
+                real_method = "POST"
+
+        if real_method == "POST":
+            return self.update_impl(request, model_name, None, False)
+        elif real_method == "GET":
+            return self.get_all_impl(request, model_name)
+
+    def get_all_impl(self, request, model_name):
+        """Actual implementation of REST query.  Gets Model instances
+        based on criteria specified in the query parameters.
+        """
+        model_handler = self.get_model_handler(model_name)
+
+        cur_fetch_page_size = MAX_FETCH_PAGE_SIZE
+        fetch_offset = 0
+        ordering = None
+        query_expr = None
+        query_params = []
+        list_props = {}
+        
+        for arg in request.args:
+            if(arg == QUERY_OFFSET_PARAM):
+                fetch_offset = int(request.args.get(QUERY_OFFSET_PARAM))
+                continue
+            
+            if(arg == QUERY_PAGE_SIZE_PARAM):
+                cur_fetch_page_size = int(request.args.get(
+                    QUERY_PAGE_SIZE_PARAM))
+                continue
+            
+            if(arg == QUERY_ORDERING_PARAM):
+                ordering = request.args.get(QUERY_ORDERING_PARAM)
+                continue
+            
+            match = QUERY_TERM_PATTERN.match(arg)
+            if(match is None):
+                logging.warning("ignoring unexpected query param %s", arg)
+                continue
+
+            query_type = match.group(1)
+            query_field = match.group(2)
+            query_sub_expr = QUERY_EXPRS[query_type]
+
+            query_values = request.values.getlist(arg)
+            if(query_type == QUERY_LIST_TYPE):
+                query_values = [v.split(",") for v in query_values]
+
+            query_field, query_values = model_handler.read_query_values(
+              query_field, query_values)
+
+            for value in query_values:
+                query_params.append(value)
+                query_sub_expr = query_sub_expr % (query_field,
+                                                   len(query_params))
+                if(not query_expr):
+                    query_expr = QUERY_PREFIX + query_sub_expr
+                else:
+                    query_expr += QUERY_JOIN + query_sub_expr
+
+        cur_fetch_page_size = max(min(self.fetch_page_size,
+                                      cur_fetch_page_size,
+                                      MAX_FETCH_PAGE_SIZE), 1)
+
+        # if possible, attempt to fetch more than we really want so
+        # that we can determine if we have more results
+        tmp_fetch_page_size = cur_fetch_page_size
+        if(tmp_fetch_page_size < MAX_FETCH_PAGE_SIZE):
+            tmp_fetch_page_size += 1
+
+        models = model_handler.get_all(tmp_fetch_page_size, fetch_offset,
+                                       ordering, query_expr, query_params)
+
+        next_fetch_offset = str(cur_fetch_page_size + fetch_offset)
+        if((tmp_fetch_page_size > cur_fetch_page_size) and \
+             (len(models) < tmp_fetch_page_size)):
+            next_fetch_offset = ""
+
+        list_props[QUERY_OFFSET_PARAM] = next_fetch_offset
+        
+        # trim list to the actual size we want
+        if(len(models) > cur_fetch_page_size):
+            models = models[0:cur_fetch_page_size]
+
+        return self.out_to_response(
+          self.models_to_xml(request, model_name, model_handler, models,
+                             list_props))
+
+
+    def update_impl(self, request, model_name, model_key, is_replace):
+        """Actual implementation of all Model update methods.
+        Creates/updates/replaces Model instances as specified.  Writes
+        the key of the modified Model as a plain text result.
+        """
+        
+        model_handler = self.get_model_handler(model_name)
+        if(not model_handler):
+            return
+
+        is_list = False
+        if request.content_type.startswith(JSON_CONTENT_TYPE):
+            try:
+                model_dict = simplejson.load(request.stream)
+                model_els = [(model_key, model_dict)]
+                if isinstance(model_dict, list):
+                    is_list = True
+                    model_els = []
+                    for item in model_dict:
+                        model_els.append((MULTI_UPDATE_KEY, item))
+                models = []
+                for model_el_key, model_el in model_els:
+                    models.append(self.model_from_dict(
+                        model_el, model_name, model_handler, model_el_key,
+                        is_replace))
+            except Exception:
+                logging.exception("failed parsing model")
+                raise BadRequest()
+        else:
+            doc = minidom.parse(request.stream)
+
+            model_els = [(model_key, doc.documentElement)]
+            if(str(doc.documentElement.nodeName) == LIST_EL_NAME):
+                is_list = True
+                model_els = []
+                for node in doc.documentElement.childNodes:
+                    if(node.nodeType == node.ELEMENT_NODE):
+                        model_els.append((MULTI_UPDATE_KEY, node))
+
+            models = []
+            try:
+                for model_el_key, model_el in model_els:
+                    models.append(self.model_from_xml(
+                        model_el, model_name, model_handler, model_el_key,
+                        is_replace))
+            except Exception:
+                logging.exception("failed parsing model")
+                raise BadRequest()
+            finally:
+                doc.unlink()
+
+        db.put(models)
+
+        # if input was not a list, convert single element models list
+        # back to single element
+        if(not is_list):
+            models = models[0]
+
+        # note, we specifically look in the query string (don't try to
+        # parse the POST body)
+        output_type = request.args.get("type", None)
+        if output_type == "full":
+            return self.out_to_response(
+              self.models_to_xml(request, model_name, model_handler, models))
+        elif output_type == "xml":
+            return self.out_to_response(
+              self.keys_to_xml(request, model_handler, models))
+        else:
+            return self.out_to_response(self.keys_to_text(models))
+
+    def get_model_handler(self, model_name):
+        try:
+            self._import_model_if_not(model_name)
+            return self.model_handlers[model_name]
+        except Exception:
+            raise NotFound()
+
+    def doc_to_output(self, request, doc):
+        out_mime_type = request.accept_mimetypes.best_match(
+          [JSON_CONTENT_TYPE, XML_CONTENT_TYPE])
+        if out_mime_type == JSON_CONTENT_TYPE:
+          return xml_to_json(doc)
+        else:
+          return doc.toxml(XML_ENCODING)
+
+    def out_to_response(self, output):
+         first_char = output[0]
+         content_type = TEXT_CONTENT_TYPE
+         if(first_char == '{'):
+             content_type = JSON_CONTENT_TYPE
+         elif(first_char == "<"):
+             content_type = XML_CONTENT_TYPE
+         return Response(output, content_type=content_type)
+
+    def models_to_xml(self, request, model_name, model_handler, models,
+                      list_props=None):
+        """Returns a string of xml of the given models (may be list or
+        single instance)."""
+        impl = minidom.getDOMImplementation()
+        doc = None
+        try:
+            if isinstance(models, (types.ListType, types.TupleType)):
+                doc = impl.createDocument(None, LIST_EL_NAME, None)
+                list_el = doc.documentElement
+                if ((list_props is not None) and \
+                      (list_props.has_key(QUERY_OFFSET_PARAM))):
+                    list_el.attributes[QUERY_OFFSET_PARAM] = \
+                        list_props[QUERY_OFFSET_PARAM]
+                for model in models:
+                    model_el = append_child(list_el, model_name)
+                    model_handler.write_xml_value(model_el, model)
+            else:
+                doc = impl.createDocument(None, model_name, None)
+                model_handler.write_xml_value(doc.documentElement, models)
+
+            return self.doc_to_output(request, doc)
+        finally:
+            if doc:
+                doc.unlink()
+
+    def keys_to_xml(self, request, model_handler, models):
+        """Returns a string of xml of the keys of the given models
+        (may be list or single instance)."""
+        impl = minidom.getDOMImplementation()
+        doc = None
+        try:
+            if isinstance(models, (types.ListType, types.TupleType)):
+                doc = impl.createDocument(None, LIST_EL_NAME, None)
+                list_el = doc.documentElement
+                    
+                for model in models:
+                    append_child(
+                        list_el, KEY_PROPERTY_NAME,
+                        model_handler.key_handler.get_value_as_string(model))
+            else:
+                doc = impl.createDocument(None, KEY_PROPERTY_NAME, None)
+                doc.documentElement.appendChild(
+                    doc.createTextNode(
+                        model_handler.key_handler.get_value_as_string(models)))
+            return self.doc_to_output(request, doc)
+        finally:
+            if doc:
+                doc.unlink()
+
+    def keys_to_text(self, models):
+        """Returns a string of text of the keys of the given models
+        (may be list or single instance)."""
+        if(not isinstance(models, (types.ListType, types.TupleType))):
+            models = [models]
+        return unicode(",".join([str(model.key()) for model in models]))
+
+    def model_from_dict(self, json, model_name, model_handler, key,
+                        is_replace):
+        """Returns a model instance updated from the given model json
+        element."""
+        if not json.has_key(model_name):
+            raise TypeError("wrong model name, found '%s', expected '%s'" %
+                            (json.keys()[0], model_name))
+        props = model_handler.read_dict_value(json)
+        given_key = props.pop(KEY_PROPERTY_NAME, None)
+
+        if(key is MULTI_UPDATE_KEY):
+            if(given_key):
+                key = str(given_key)
+            else:
+                key = None
+
+        if(key):
+            key = db.Key(key.strip())
+            if(given_key and (given_key != key)):
+                raise ValueError("key in data %s does not match request key %s"
+                                 % (given_key, key))
+            model = model_handler.get(key)
+
+            if(is_replace):
+                for prop_name, prop_type in model.properties().iteritems():
+                    setattr(model, prop_name, prop_type.default_value())
+                for prop_name in model.dynamic_properties():
+                    delattr(model, prop_name)
+
+            for prop_name, prop_value in props.iteritems():
+                setattr(model, prop_name, prop_value)
+                
+        else:
+            model = model_handler.create(props)
+
+        return model
+        
+    def model_from_xml(self, model_el, model_name, model_handler, key,
+                       is_replace):
+        """Returns a model instance updated from the given model xml
+        element."""
+        if(model_name != str(model_el.nodeName)):
+            raise TypeError("wrong model name, found '%s', expected '%s'" %
+                            (model_el.nodeName, model_name))
+        props = model_handler.read_xml_value(model_el)
+
+        given_key = props.pop(KEY_PROPERTY_NAME, None)
+
+        if(key is MULTI_UPDATE_KEY):
+            if(given_key):
+                key = str(given_key)
+            else:
+                key = None
+        
+        if(key):
+            key = db.Key(key.strip())
+            if(given_key and (given_key != key)):
+                raise ValueError("key in data %s does not match request key %s"
+                                 % (given_key, key))
+            model = model_handler.get(key)
+
+            if(is_replace):
+                for prop_name, prop_type in model.properties().iteritems():
+                    setattr(model, prop_name, prop_type.default_value())
+                for prop_name in model.dynamic_properties():
+                    delattr(model, prop_name)
+
+            for prop_name, prop_value in props.iteritems():
+                setattr(model, prop_name, prop_value)
+                
+        else:
+            model = model_handler.create(props)
+
+        return model

kay/tests/__init__.py

 from utils_test import *
 from session_test import *
 from auth_test import *
+from rest_test import *

kay/tests/rest_settings.py

+# -*- coding: utf-8 -*-
+
+"""
+Kay test settings.
+
+:Copyright: (c) 2009 Takashi Matsuo <tmatsuo@candit.jp> All rights reserved.
+:license: BSD, see LICENSE for more details.
+"""
+
+DEBUG = False
+ROOT_URL_MODULE = 'kay.tests.globalurls'
+
+MIDDLEWARE_CLASSES = (
+  'kay.auth.middleware.AuthenticationMiddleware',
+)
+
+INSTALLED_APPS = (
+  'kay.tests.restapp',
+)
+
+APP_MOUNT_POINTS = {
+  'kay.tests.restapp': '/',
+}

kay/tests/rest_test.py

+
+from werkzeug import (
+  BaseResponse, Request
+)
+from werkzeug.datastructures import Headers
+
+from kay.utils.test import Client
+from kay.app import get_application
+from kay.conf import LazySettings
+from kay.ext.testutils.gae_test_base import GAETestBase
+from kay.utils import url_for
+
+from kay.tests.restapp.models import RestModel
+
+class RestTestCase(GAETestBase):
+  KIND_NAME_UNSWAPPED = False
+  USE_PRODUCTION_STUBS = True
+  CLEANUP_USED_KIND = True
+
+  def setUp(self):
+    s = LazySettings(settings_module='kay.tests.rest_settings')
+    app = get_application(settings=s)
+    self.client = Client(app, BaseResponse)
+
+  def tearDown(self):
+    pass
+
+  def test_rest_json(self):
+
+    headers = Headers({"Accept": "application/json"})
+
+    response = self.client.get('/rest/metadata', headers=headers)
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.get('/rest/metadata/RestModel', headers=headers)
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.post(
+      '/rest/RestModel',
+      data='{"RestModel": {"i_prop": 12, "s_prop": "string"}}',
+      content_type="application/json; charset=utf-8")
+    self.assertEqual(response.status_code, 200)
+    key = response.data
+    elm = RestModel.get(key)
+    self.assertEqual(elm.s_prop, "string")
+    self.assertEqual(elm.i_prop, 12)
+
+    response = self.client.post(
+      '/rest/RestModel/%s' % key,
+      data='{"RestModel": {"i_prop": 14}}',
+      content_type="application/json; charset=utf-8")
+    self.assertEqual(response.status_code, 200)
+    key2 = response.data
+    self.assertEqual(key, key2)
+    elm = RestModel.get(key)
+    self.assertEqual(elm.s_prop, "string")
+    self.assertEqual(elm.i_prop, 14)
+    
+    response = self.client.post(
+      '/rest/RestModel',
+      data='[{"RestModel": {"i_prop": 1, "s_prop": "foobar1"}},{"RestModel": {"i_prop": 2, "s_prop": "foobar2"}}]',
+      content_type="application/json; charset=utf-8")
+    self.assertEqual(response.status_code, 200)
+    key3, key4 = response.data.split(',')
+    elm3 = RestModel.get(key3)
+    elm4 = RestModel.get(key4)
+    self.assertEqual(elm3.s_prop, "foobar1")
+    self.assertEqual(elm3.i_prop, 1)
+    self.assertEqual(elm4.s_prop, "foobar2")
+    self.assertEqual(elm4.i_prop, 2)
+
+    response = self.client.get('/rest/RestModel', headers=headers)
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.get('/rest/RestModel/%s' % key, headers=headers)
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.get('/rest/RestModel/%s/s_prop' % key,
+                               headers=headers)
+    self.assertEqual(response.status_code, 200)
+    self.assertEqual(response.data, "string")
+
+    response = self.client.get('/rest/RestModel/%s/i_prop' % key,
+                               headers=headers)
+    self.assertEqual(response.status_code, 200)
+    self.assertEqual(response.data, "14")
+
+    response = self.client.delete('/rest/RestModel/%s' % key,
+                                  headers=headers)
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.get('/rest/RestModel/%s' % key,
+                               headers=headers)
+    self.assertEqual(response.status_code, 404)
+
+
+  def test_rest_operations(self):
+    response = self.client.get('/rest/metadata')
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.get('/rest/metadata/RestModel')
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.post('/rest/RestModel', data='<?xml version="1.0" encoding="utf-8"?><RestModel><i_prop>12</i_prop><s_prop>string</s_prop></RestModel>')
+    self.assertEqual(response.status_code, 200)
+    key = response.data
+    elm = RestModel.get(key)
+    self.assertEqual(elm.s_prop, "string")
+    self.assertEqual(elm.i_prop, 12)
+
+    response = self.client.post(
+      '/rest/RestModel/%s' % key,
+      data='<?xml version="1.0" encoding="utf-8"?><RestModel><i_prop>14</i_prop></RestModel>')
+    self.assertEqual(response.status_code, 200)
+    key2 = response.data
+    self.assertEqual(key, key2)
+    elm = RestModel.get(key)
+    self.assertEqual(elm.s_prop, "string")
+    self.assertEqual(elm.i_prop, 14)
+
+    response = self.client.get('/rest/RestModel')
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.get('/rest/RestModel/%s' % key)
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.get('/rest/RestModel/%s/s_prop' % key)
+    self.assertEqual(response.status_code, 200)
+    self.assertEqual(response.data, "string")
+
+    response = self.client.get('/rest/RestModel/%s/i_prop' % key)
+    self.assertEqual(response.status_code, 200)
+    self.assertEqual(response.data, "14")
+
+    response = self.client.delete('/rest/RestModel/%s' % key)
+    self.assertEqual(response.status_code, 200)
+
+    response = self.client.get('/rest/RestModel/%s' % key)
+    self.assertEqual(response.status_code, 404)
+    

kay/tests/restapp/__init__.py

+#

kay/tests/restapp/models.py

+
+from google.appengine.ext import db
+
+class RestModel(db.Model):
+  s_prop = db.StringProperty()
+  i_prop = db.IntegerProperty()
+  

kay/tests/restapp/urls.py

+# -*- coding: utf-8 -*-
+
+"""
+Kay test urls.
+
+:Copyright: (c) 2009 Takashi Matsuo <tmatsuo@candit.jp> All rights reserved.
+:license: BSD, see LICENSE for more details.
+"""
+
+from kay.generics.rest import RESTViewGroup
+
+class MyRESTViewGroup(RESTViewGroup):
+  models = [
+    'kay.tests.restapp.models.RestModel',
+  ]
+
+view_groups = [
+  MyRESTViewGroup(),
+]