[cc-commits] [SCM] cc.api based on sane cc.license (branch master) updated. 6878ef92b9e1324eb9c90cbd99081981d2d564d0

git version control git at a7.creativecommons.org
Fri Mar 5 18:59:25 EST 2010


The branch, master has been updated
       via  6878ef92b9e1324eb9c90cbd99081981d2d564d0 (commit)
       via  c503ffa55dc4a6f5d6d778409a0d1f21481a21ec (commit)
       via  4278b9fd2e266fde1d9b076ef17770f04bef6941 (commit)
       via  df253c9ac71fed2d48e21eab9ebc1a8c1382bf14 (commit)
       via  93f34517b46c8a4c0cae19991aa64d7521847988 (commit)
       via  60937c28e980142e67ca081e5b61b1543695d52d (commit)
      from  64768d5150b100943f24b94b7c675e21d4629f22 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 6878ef92b9e1324eb9c90cbd99081981d2d564d0
Author: John Doig <jed at jedpad.(none)>
Date:   Fri Mar 5 15:56:59 2010 -0800

    Adding details resource

commit c503ffa55dc4a6f5d6d778409a0d1f21481a21ec
Author: John Doig <jed at jedpad.(none)>
Date:   Fri Mar 5 15:56:40 2010 -0800

    New approach to content_types decorator
    
    Removing dependency of mimerender, renamed 'emitters' module to 'handlers' which is a more conventional and intuitive name, and lastly updated import paths of the resources

commit 4278b9fd2e266fde1d9b076ef17770f04bef6941
Author: John Doig <jed at jedpad.(none)>
Date:   Thu Mar 4 18:01:05 2010 -0800

    Resources should return lxml Elements, its just simpler this way.

commit df253c9ac71fed2d48e21eab9ebc1a8c1382bf14
Author: John Doig <jed at jedpad.(none)>
Date:   Thu Mar 4 10:41:57 2010 -0800

    Fixing import paths

commit 93f34517b46c8a4c0cae19991aa64d7521847988
Author: John Doig <jed at jedpad.(none)>
Date:   Thu Mar 4 10:41:24 2010 -0800

    Establishing entry points for the cc.api

commit 60937c28e980142e67ca081e5b61b1543695d52d
Author: John Doig <jed at jedpad.(none)>
Date:   Wed Mar 3 15:44:05 2010 -0800

    Modifying old api test suite to suit our current needs, a few of these test cases actually passed on first run ;)

-----------------------------------------------------------------------

Summary of changes (followed by patch):
 buildout.cfg                 |    1 +
 cc/__init__.py               |    2 +-
 cc/api/api_exceptions.py     |   10 +++
 cc/api/app.py                |   19 +++---
 cc/api/emitters.py           |  147 ------------------------------------------
 cc/api/handlers.py           |  122 ++++++++++++++++++++++++++++++++++
 cc/api/resources/base.py     |   28 ++++----
 cc/api/resources/details.py  |   80 +++++++++++++++++++++++
 cc/api/resources/license.py  |   66 ++++++++-----------
 cc/api/resources/locales.py  |   14 +++-
 cc/api/{app.py => server.py} |   49 ++++++++------
 cc/api/tests/test_common.py  |   32 ++++------
 cc/api/tests/test_details.py |    2 +-
 cc/api/tests/test_license.py |    2 +-
 cc/api/tests/test_locales.py |    2 +-
 cc/api/tests/test_root.py    |    2 +-
 cc/api/tests/test_simple.py  |    2 +-
 cc/api/tests/test_support.py |    2 +-
 setup.py                     |   47 ++++++++++---
 19 files changed, 359 insertions(+), 270 deletions(-)
 delete mode 100644 cc/api/emitters.py
 create mode 100644 cc/api/handlers.py
 copy cc/api/{app.py => server.py} (62%)

diff --git a/buildout.cfg b/buildout.cfg
index b95da2d..e5b886a 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -5,6 +5,7 @@ find-links = http://a9.creativecommons.org/~cwebber/eggs/
 
 [cc.api]
 recipe = zc.recipe.egg
+eggs = cc.api[fcgi]
 interpreter = python
 entry-points = 
     nosetests=nose:main
diff --git a/cc/__init__.py b/cc/__init__.py
index 8b13789..de40ea7 100644
--- a/cc/__init__.py
+++ b/cc/__init__.py
@@ -1 +1 @@
-
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/cc/api/api_exceptions.py b/cc/api/api_exceptions.py
index affb923..978e9a3 100644
--- a/cc/api/api_exceptions.py
+++ b/cc/api/api_exceptions.py
@@ -18,6 +18,16 @@
 ## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 ## DEALINGS IN THE SOFTWARE.
 
+def missingparam(param):
+    return {'error':{'id':{'@text':'missingparam'},
+                     'message':{
+                         '@text':'A value for %s must be supplied.' % param }}}
+
 def invalidclass():
     return {'error':{'id':{'@text':'invalidclass'},
                      'message':{'@text':'Invalid License Class.'}}}
+
+def invaliduri():
+    return {'error':{'id':{'@text':'invaliduri'},
+                     'message':{'@text':'Invalid license uri.'}}}
+    
diff --git a/cc/api/app.py b/cc/api/app.py
index c01aa55..b6fbca1 100644
--- a/cc/api/app.py
+++ b/cc/api/app.py
@@ -23,19 +23,20 @@ web.config.debug = True
 
 urls = ( # tuple of url to resource method mappings
     
-    '/',        'resources.base.index',
-    '/locales', 'resources.locales.index',
-    '/details', 'resources.details.index',
+    '/',        'cc.api.resources.base.index',
+    '/classes', 'cc.api.resources.base.index',
+    '/locales', 'cc.api.resources.locales.index',
+    '/details', 'cc.api.resources.details.index',
     
-    '/license/([a-z]+)', 'resources.license.index',
-    '/license/([a-z]+)/(issue|get)', 'resources.license.issue',
+    '/license/([a-z]+)', 'cc.api.resources.license.index',
+    '/license/([a-z]+)/(issue|get)', 'cc.api.resources.license.issue',
     
-    '/simple/chooser',  'resources.simple.chooser',
-    '/support/jurisdictions', 'resources.support.jurisdictions',
+    '/simple/chooser',  'cc.api.resources.simple.chooser',
+    '/support/jurisdictions', 'cc.api.resources.support.jurisdictions',
 
     ) 
     
-app = web.application(urls, globals(),)
+application = web.application(urls, globals(),)
 
 if __name__ == "__main__":
-    app.run()
+    application.run()
diff --git a/cc/api/emitters.py b/cc/api/emitters.py
deleted file mode 100644
index 23085af..0000000
--- a/cc/api/emitters.py
+++ /dev/null
@@ -1,147 +0,0 @@
-## Copyright (c) 2010, John Doig, Creative Commons
-
-## Permission is hereby granted, free of charge, to any person obtaining
-## a copy of this software and associated documentation files (the "Software"),
-## to deal in the Software without restriction, including without limitation
-## the rights to use, copy, modify, merge, publish, distribute, sublicense,
-## and/or sell copies of the Software, and to permit persons to whom the
-## Software is furnished to do so, subject to the following conditions:
-
-## The above copyright notice and this permission notice shall be included in
-## all copies or substantial portions of the Software.
-
-## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-## DEALINGS IN THE SOFTWARE.
-
-import lxml.etree as ET
-import re
-import json
-import web.webapi
-import mimerender
-from decorator import decorator
-
-class Emitter(object):
-    def format(self, **results):
-        return results
-
-class JSONEmitter(Emitter):
-    def format(self, **results):
-        return json.dumps(results)
-
-class XMLEmitter(Emitter):
-
-    XML_NS = 'http://www.w3.org/XML/1998/namespace'
-    attrib_key = '@attributes'
-    text_key = '@text'
-
-    def child_nodes(self, node):
-        return filter(lambda e: e not in (self.attrib_key,self.text_key), node)
-    
-    def build_element(self, node, attrib=None, text=None):
-        # need to expand prefixes
-
-        if type(attrib) == dict and attrib.get('lang'):
-            attrib['{%s}lang' % self.XML_NS] = attrib['lang']
-            del attrib['lang']
-            
-        ele = ET.Element(node, attrib)
-        ele.text = text
-        return ele
-    
-    def build_tree(self, parent, children):
-        
-        for c in self.child_nodes(children):
-            if type(children[c]) == list:
-                for obj in children[c]:
-                    if type(obj) == list:
-                        ele = self.build_element(c)
-                        self.build_tree(ele, obj)
-                    elif type(obj == dict):
-                        ele = self.build_element(c,
-                                                 obj.get(self.attrib_key),
-                                                 obj.get(self.text_key))
-                        if self.child_nodes(obj):
-                            self.build_tree(ele, obj)
-                    parent.append(ele)
-            else:
-                if type(children[c]) == dict:
-                    ele = self.build_element(c,
-                                             children[c].get(self.attrib_key),
-                                             children[c].get(self.text_key))
-                    if self.child_nodes(children[c]):
-                        self.build_tree(ele, children[c])
-                else:
-                    ele = self.build_element(c)
-                parent.append(ele)        
-        return parent
-
-    def dict_to_etree(self, d):
-        top = list(d)[0]
-        attrib, text = None, None
-        if type(d[top]) == dict:
-            attrib = d[top].get(self.attrib_key, None)
-            text = d[top].get(self.text_key, None)
-        root = self.build_element(top, attrib, text)
-        return self.build_tree(root, d[top])
-    
-    def format(self, **results):
-        return ET.tostring(self.dict_to_etree(results))
-
-class HTMLEmitter(XMLEmitter):
-    def format(self, **results):
-        # use XMLEmitter's format logic, but set Content-Type to text/html
-        return super(HTMLEmitter, self).format(results)
-
-formatters = {
-    mimerender.XML : XMLEmitter,
-    mimerender.JSON: JSONEmitter,
-    mimerender.HTML: HTMLEmitter,
-}
-
-def contenttypes(*types):
-    """
-    contenttypes is a wrapper to the mimerender decorator.
-
-    When the contenttypes decorator is used, it is to be passed a
-    list of string identifying the supported formats for that resource.
-    
-    The list of available short names to the media-types can be found in
-    the keys of the formatters dictionary.
-    
-    TODO finish documenting here, expecially to explain mimerender
-    """
-
-    emitters = {}
-    for t in types:
-       if t in formatters.keys():
-           # all emitter classes must implement format
-           emitters[t] = formatters[t]().format 
-       # TODO: maybe log/catch if type is unsupported, fallback to text?
-
-    # default to the first type passed to decorator 
-    default = types[0] in formatters.keys() and types[0] or 'text'
-    
-    # mimerender will however throw an exception for invalid formats,
-    # to catch these exceptions the mimerender's decorator is wrapped
-    # TODO this needs better explanation
-    def wrapper(f, *args, **kwargs):
-        """
-        mimerender will handle the content negotation based on the headers
-        and the query string parameters. emitters is now a mapping of
-        media-type short names to a method that can emit the resources'
-        returned dicts as xml, json, etc as HTTP responses.
-        """
-        try:    
-            return mimerender.mimerender(default,
-                                         override_input_key='format',
-                                         **emitters)(f)(*args, **kwargs)
-        except (mimerender.MimeRenderException, ValueError):
-            web.webapi.badrequest()
-
-    return decorator(wrapper)
-
diff --git a/cc/api/handlers.py b/cc/api/handlers.py
new file mode 100644
index 0000000..e203cc5
--- /dev/null
+++ b/cc/api/handlers.py
@@ -0,0 +1,122 @@
+## Copyright (c) 2010, John Doig, Creative Commons
+
+## Permission is hereby granted, free of charge, to any person obtaining
+## a copy of this software and associated documentation files (the "Software"),
+## to deal in the Software without restriction, including without limitation
+## the rights to use, copy, modify, merge, publish, distribute, sublicense,
+## and/or sell copies of the Software, and to permit persons to whom the
+## Software is furnished to do so, subject to the following conditions:
+
+## The above copyright notice and this permission notice shall be included in
+## all copies or substantial portions of the Software.
+
+## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+## DEALINGS IN THE SOFTWARE.
+
+import web
+import lxml.etree as ET
+import mimeparse
+from decorator import decorator
+
+class HandlerClass(type):
+    """ metaclass used in request handlers """
+    handlers = {}
+    types = {}
+    def __new__(meta, classname, bases, classDict):
+        # keep track of the available `Handler`s
+        new_class = type.__new__(meta, classname, bases, classDict)
+        # 'xml' => XMLHandler, 'html' => HTMLHandler, etc.
+        meta.handlers[new_class.short_name] = new_class
+        # 'application/xml' => XMLHandler, 'text/html' => HTMLHandler
+        meta.types.update(dict([(t,new_class)for t in new_class.content_types]))
+
+        return new_class
+
+class Handler(object):
+    """ Uses the HandlerClass metaclass for registering which Handler
+    handles which content-type and mapping the short type identifiers
+    used in the decorator to class capable of handlingt that format.
+
+    To extend the handlers available, simply create a class that inherits
+    the `Handler` class and implement a method named `repsonse` that returns
+    a string response message.
+    
+    """
+    __metaclass__ = HandlerClass
+    # the first content_type will be used in the response headers
+    content_types = ('text/plain',)
+    # this will be the string used in the @content_types decorator
+    short_name = 'default'
+    # if a response needs to be encoded otherwise, this can be overloaded
+    charset = 'utf-8'
+    def __init__(self, *args, **kwargs):
+        # set the HTTP Content-Type response header 
+        web.header('Content-Type', '%s; charset=%s' % (self.content_types[0],
+                                                       self.charset))
+    def response(self, results):
+        # all resources that use @content_types will return an lxml etree object
+        return ET.tostring(results)
+
+class XMLHandler(Handler):
+    content_types = ('application/xml', 'application/x-xml', 'text/xml',)
+    short_name = 'xml'
+    def response(self, results):
+        """ serialize the results ElementTree object """
+        sig = '<?xml version="1.0" encoding="utf-8"?>\n'
+        return sig + ET.tostring(results, pretty_print=True)
+
+class HTMLHandler(Handler):
+    content_types = ('text/html',)
+    short_name = 'html'
+    def response(self, results):
+        """ format the results as html """
+        return ET.tostring(results, pretty_print=True)
+
+def content_types(*types):
+    def wrap(fn, *args, **kwargs):
+
+        # use mimeparse to parse the http Accept header string
+        # restrict matches to the content types supported 
+        mime_type = mimeparse.best_match(HandlerClass.types.keys(),
+                                         web.ctx.env.get('HTTP_ACCEPT')) 
+        
+        if mime_type:
+            # find the handler for the requested mime-type
+            try:
+                handler = HandlerClass.types[mime_type]
+
+                # only use handlers passed into the decorator
+                if handler.short_name not in types:
+                    return web.webapi.badrequest()
+
+            except KeyError:
+                # this mimetype is unsupported
+                # fallback to a default Handler, which would be the first
+                # handler shortname passed to the decorator
+                return web.webapi.badrequest()
+        else:
+            # if no match is found by mimeparse, then an empty string is
+            # returned need to fall back to the default handler in this case
+            try:
+                handler = HandlerClass.handlers['default']
+            except KeyError:
+                raise Exception("A default handler has not been set.")
+        try:
+            # if results is not an lxml Element, then the handler will
+            # throw an exception. if the resource resulted in anything
+            # other than an xml tree then this decorator should not be
+            # used for that resource's method.
+            
+            return handler().response(fn(*args, **kwargs))
+            
+        except TypeError:
+
+            # this decorator is only good for results that are serializable
+            return web.internalerror()
+        
+    return decorator(wrap)
diff --git a/cc/api/resources/base.py b/cc/api/resources/base.py
index 9289f3b..f3b0f7b 100644
--- a/cc/api/resources/base.py
+++ b/cc/api/resources/base.py
@@ -20,21 +20,23 @@
 
 import cc.license
 import web
-from emitters import contenttypes
+import lxml.etree as ET
+
+from cc.api.handlers import content_types
 
 class index:
-    @contenttypes('xml', 'json')
+    
+    @content_types('xml', 'json')
     def GET(self):
         """ Returns a list of available license for a given locale. """
+
         locale = web.input().get('locale', 'en')
-        classes = cc.license.selectors.SELECTORS    
-        return {
-            'licenses': {
-                'license': [
-                    {
-                        '@attributes' : {'id': selector },
-                        '@text' : lclass.title(locale),
-                    }
-                    for selector, lclass in classes.iteritems() ]
-                }
-            }
+        classes = cc.license.selectors.SELECTORS
+
+        root = ET.Element('licenses')
+        for selector, lclass in classes.iteritems():
+            ET.SubElement(root, 'license', dict(id=selector)).text = \
+                                lclass.title(locale)
+
+        return root
+        
diff --git a/cc/api/resources/details.py b/cc/api/resources/details.py
index e69de29..bf1fd82 100644
--- a/cc/api/resources/details.py
+++ b/cc/api/resources/details.py
@@ -0,0 +1,80 @@
+## Copyright (c) 2010, John Doig, Creative Commons
+
+## Permission is hereby granted, free of charge, to any person obtaining
+## a copy of this software and associated documentation files (the "Software"),
+## to deal in the Software without restriction, including without limitation
+## the rights to use, copy, modify, merge, publish, distribute, sublicense,
+## and/or sell copies of the Software, and to permit persons to whom the
+## Software is furnished to do so, subject to the following conditions:
+
+## The above copyright notice and this permission notice shall be included in
+## all copies or substantial portions of the Software.
+
+## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+## DEALINGS IN THE SOFTWARE.
+
+import web
+import cc.license
+import lxml.etree as ET
+from StringIO import StringIO
+from copy import deepcopy
+
+from cc.license.formatters.classes import HTMLFormatter, CC0HTMLFormatter
+from cc.api.api_exceptions import missingparam, invaliduri
+from cc.api.handlers import content_types
+
+class index:
+    
+    @content_types('xml', 'html')
+    def GET(self):
+        """ Accepts a license uri as an argument and will return
+        the RDF and RDFa of a licnsee """
+
+        license_uri = web.input().get('license-uri')
+        if not license_uri:
+            return missingparam('license-uri')
+
+        try:
+            l = cc.license.by_uri(str(license_uri))
+        except cc.license.CCLicenseError:
+            return invaliduri()
+
+        if l.license_code == 'CC0':
+            formatter = CC0HTMLFormatter
+        else:
+            formatter = HTMLFormatter
+
+        root = ET.Element('result')
+
+        # add the license uri and name
+        ET.SubElement(root, 'license-uri').text = str(l.uri)
+        ET.SubElement(root, 'license-name').text = str(l.title())
+
+        # parse the RDF and RDFa from cc.license
+        license_rdf = ET.parse(StringIO(l.rdf))
+        license_rdfa = ET.parse(StringIO("<p>%s</p>" % formatter().format(l)))
+
+        # build an empty Work tree
+        rdfns = lambda x: '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}%s'%x
+        work = ET.Element('Work', { rdfns('about') : '' })
+        ET.SubElement(work, 'License', { rdfns('resource') : l.uri })
+        license_rdf.getroot().insert(0, work)
+
+        # add RDF trees to the results
+        ET.SubElement(root, 'rdf').append(license_rdf.getroot())
+        license_rdf = deepcopy(license_rdf)
+        ET.SubElement(root,'licenserdf').append(license_rdf.getroot())
+
+        # the html tree has a spurious element at its root that was
+        # required for the RDFa to parse, so only append the children
+        html = ET.SubElement(root, 'html')
+        for element in license_rdfa.getroot().getchildren():
+            html.append(element)
+
+        return root
+                
diff --git a/cc/api/resources/license.py b/cc/api/resources/license.py
index 8652803..0ba77c1 100644
--- a/cc/api/resources/license.py
+++ b/cc/api/resources/license.py
@@ -21,11 +21,13 @@
 
 import cc.license
 import web
-import api_exceptions
-from emitters import contenttypes
+import lxml.etree as ET
+
+from cc.api import api_exceptions
+from cc.api.handlers import content_types
 
 class index:
-    @contenttypes('xml', 'json')
+    @content_types('xml', 'json')
     def GET(self, selector):
         
         try:
@@ -35,44 +37,32 @@ class index:
 
         locale = web.input().get('locale', 'en')
 
-        questions = []
+        root = ET.Element('licenseclass', dict(id=selector))
+        label = ET.SubElement(root, 'label', dict(lang=locale))
+        label.text = lclass.title(locale)
+        
         for question in lclass.questions():
 
-            enums = []
-            for label, value in question.answers():
-                enum = {
-                    '@attributes': {'id': value},
-                    'label': {
-                        '@attributes': {'lang': locale},
-                        '@text' : label,
-                        },
-                    }
-                if hasattr(enum, "description"):
-                    enum['description'] = {
-                        '@attributes': {'lang': locale},
-                        '@text': 'Uhm???',
-                        }
-                    
-                enums.append(enum)
+            field = ET.SubElement(root, 'field', dict(id=question.id))
+            label = ET.SubElement(field, 'label', dict(lang=locale))
+            label.text = question.label(locale)
+
+            ET.SubElement(field, 'type').text = 'enum'
+
+            for a_label, a_id, a_desc in question.answers():
+                
+                enum = ET.SubElement(field, 'enum', dict(id=a_id))
+                label = ET.SubElement(enum, 'label', dict(lang=locale))
+                label.text = a_label
+
+                if a_desc:
+                    ET.SubElement(enum,
+                                  'description',
+                                  dict(lang=locale)).text = a_desc
                     
-            question = {'@attributes':{'id': question.id},
-                        'label':{
-                            '@attributes':{'lang':locale},
-                            '@text': question.label(locale)},
-                        'type': {
-                            '@text': 'enum'},
-                        'enum': enums,
-                        'description':{
-                            '@attributes':{'lang':locale},
-                            '@text': question.description(locale)}}
-            questions.append(question)
-            
-        return {'licenseclass':{'@attributes':{'id':selector},
-                                'label':{
-                                    '@attributes': {'lang': locale},
-                                    '@text': lclass.title(locale)},
-                                'field': questions}}
+            desc = ET.SubElement(field, 'description', dict(lang=locale))
+            desc.text = question.description(locale)
 
-        
+        return root
 
         
diff --git a/cc/api/resources/locales.py b/cc/api/resources/locales.py
index 011ee5c..f340456 100644
--- a/cc/api/resources/locales.py
+++ b/cc/api/resources/locales.py
@@ -19,12 +19,18 @@
 ## DEALINGS IN THE SOFTWARE.
 
 import cc.license
-from emitters import contenttypes
+import lxml.etree as ET
+
+from cc.api.handlers import content_types
 
 class index:
-    @contenttypes('xml', 'json')
+    @content_types('xml', 'json')
     def GET(self):
         """ Return a list of the currently supported locales """
         locales = cc.license.locales()
-        return {'locales': { 'locale': [
-                {'@attributes': { 'id': l } } for l in locales ]}}
+
+        root = ET.Element('locales')
+        for l in locales:
+            ET.SubElement(root, 'locale', dict(id=l))
+
+        return root
diff --git a/cc/api/app.py b/cc/api/server.py
similarity index 62%
copy from cc/api/app.py
copy to cc/api/server.py
index c01aa55..610097a 100644
--- a/cc/api/app.py
+++ b/cc/api/server.py
@@ -1,4 +1,4 @@
-## Copyright (c) 2010, John Doig, Creative Commons
+## Copyright (c) 2006-2009 Nathan R. Yergler, Creative Commons
 
 ## Permission is hereby granted, free of charge, to any person obtaining
 ## a copy of this software and associated documentation files (the "Software"),
@@ -16,26 +16,31 @@
 ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 ## FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-## DEALINGS IN THE SOFTWARE.
+## DEALINGS IN THE SOFTWARE
 
 import web
-web.config.debug = True
-
-urls = ( # tuple of url to resource method mappings
-    
-    '/',        'resources.base.index',
-    '/locales', 'resources.locales.index',
-    '/details', 'resources.details.index',
-    
-    '/license/([a-z]+)', 'resources.license.index',
-    '/license/([a-z]+)/(issue|get)', 'resources.license.issue',
-    
-    '/simple/chooser',  'resources.simple.chooser',
-    '/support/jurisdictions', 'resources.support.jurisdictions',
-
-    ) 
-    
-app = web.application(urls, globals(),)
-
-if __name__ == "__main__":
-    app.run()
+import app
+
+def develop():
+    web.webapi.internalerror = web.debugerror
+
+    app.application.run(web.reloader)
+
+def serve():
+
+    app.application.run()
+
+def app_factory(*args):
+    """Application factory for use with Python Paste deployments."""
+
+    # web.application (app.application) is a WSGI object now
+    return app.application.wsgifunc()
+
+def fcgi():
+    """Spawn an FCGI listening interface."""
+
+    web.wsgi.runwsgi = lambda func, addr=None: web.wsgi.runfcgi(func, addr)
+    app.application.run()
+
+def noop():
+    pass
diff --git a/cc/api/tests/test_common.py b/cc/api/tests/test_common.py
index 3b92e85..eecacb9 100644
--- a/cc/api/tests/test_common.py
+++ b/cc/api/tests/test_common.py
@@ -6,8 +6,12 @@ import os
 import operator
 import random
 
-import cherrypy
-import webtest # for the TestApi base class
+import webtest
+
+import cc.api.app
+
+import web
+web.config.debug = True
 
 ##################
 ## Public names ##
@@ -21,16 +25,7 @@ __all__ = (
 ###############
 ## Constants ##
 ###############
-RELAX_PATH = 'schemata'
-if not os.path.exists(RELAX_PATH):
-    RELAX_PATH = os.path.join('tests', 'schemata')
-
-CFGSTR = 'config:'
-_cfgpath = os.path.join(os.getcwd(), 'server.cfg')
-if os.path.exists(_cfgpath):
-    CFGSTR += _cfgpath
-else:
-    CFGSTR += os.path.join(os.getcwd(), '..', 'server.cfg')
+RELAX_PATH = os.path.join(os.path.dirname(__file__), 'schemata')
 
 TOO_MANY = 25
 
@@ -61,9 +56,9 @@ class TestData:
     def __init__(self):
         """Configure app to query CC API. This is for using live,
            rather than canned, data."""
-        cherrypy.config.update({ 'global' : { 'log.screen' : False, } })
-        self.app = webtest.TestApp(CFGSTR)
-
+        
+        self.app = webtest.TestApp(cc.api.app.application.wsgifunc())
+        
     def _permute(self, lists): #TODO: document function
         if lists:
             result = map(lambda i: (i,), lists[0])
@@ -158,14 +153,13 @@ class TestApi:
         """Test fixture for nosetests:
            - sets up the WSGI app server
            - creates test data generator"""
-        cherrypy.config.update({ 'global' : { 'log.screen' : False, } })
-        self.app = webtest.TestApp(CFGSTR)
+        self.app = webtest.TestApp(cc.api.app.application.wsgifunc())
         self.data = TestData()
-
+        
     def tearDown(self):
         """Test fixture for nosetests:
            - tears down the WSGI app server"""
-        cherrypy.engine.exit()
+        pass
 
     def makexml(self, bodystr):
         """Wraps text in a root element and escapes some special characters
diff --git a/cc/api/tests/test_details.py b/cc/api/tests/test_details.py
index e939ad9..19d1dc9 100644
--- a/cc/api/tests/test_details.py
+++ b/cc/api/tests/test_details.py
@@ -1,7 +1,7 @@
 
 import os
 
-from tests.test_common import *
+from cc.api.tests.test_common import *
 
 ####################
 ## Path constants ##
diff --git a/cc/api/tests/test_license.py b/cc/api/tests/test_license.py
index ca84015..37928b9 100644
--- a/cc/api/tests/test_license.py
+++ b/cc/api/tests/test_license.py
@@ -1,7 +1,7 @@
 
 import os
 
-from tests.test_common import *
+from cc.api.tests.test_common import *
 
 ####################
 ## Path constants ##
diff --git a/cc/api/tests/test_locales.py b/cc/api/tests/test_locales.py
index e2c84cd..d50254b 100644
--- a/cc/api/tests/test_locales.py
+++ b/cc/api/tests/test_locales.py
@@ -1,7 +1,7 @@
 
 import os
 
-from tests.test_common import *
+from cc.api.tests.test_common import *
 
 ####################
 ## Path constants ##
diff --git a/cc/api/tests/test_root.py b/cc/api/tests/test_root.py
index 3ddecf3..d69171d 100644
--- a/cc/api/tests/test_root.py
+++ b/cc/api/tests/test_root.py
@@ -1,7 +1,7 @@
 
 import os
 
-from tests.test_common import *
+from cc.api.tests.test_common import *
 
 ####################
 ## Path constants ##
diff --git a/cc/api/tests/test_simple.py b/cc/api/tests/test_simple.py
index 041094c..6b31c3f 100644
--- a/cc/api/tests/test_simple.py
+++ b/cc/api/tests/test_simple.py
@@ -1,7 +1,7 @@
 
 import os
 
-from tests.test_common import *
+from cc.api.tests.test_common import *
 
 ####################
 ## Path constants ##
diff --git a/cc/api/tests/test_support.py b/cc/api/tests/test_support.py
index f3211ff..09b50aa 100644
--- a/cc/api/tests/test_support.py
+++ b/cc/api/tests/test_support.py
@@ -1,7 +1,7 @@
 
 import os
 
-from tests.test_common import *
+from cc.api.tests.test_common import *
 
 ####################
 ## Path constants ##
diff --git a/setup.py b/setup.py
index c05698d..0715446 100644
--- a/setup.py
+++ b/setup.py
@@ -19,30 +19,55 @@
 ## DEALINGS IN THE SOFTWARE.
 
 from setuptools import setup, find_packages
+import sys
+
+requires = [
+    'setuptools',
+    'mimeparse',
+    'decorator',
+    'lxml',
+    'web.py',
+    'nose',
+    'cc.license',
+    'WebTest',
+    ]
+
+if sys.version_info < (2, 6):
+    requires.append('simplejson')
 
 setup(
     name = "cc.api",
     version = "0.1",
-    url = 'http://api.creativecommons.org',
     
-    packages = find_packages('cc'),
-    package_dir = {'':'cc'},
+    packages = ['cc.api'],
+    namespace_packages = ['cc'],
+    package_dir = {'':'.'},
     
     # scripts and dependencies
-    install_requires = [
-        'mimerender',
-        'decorator',
-        'lxml',
-        'web.py',
-        'cc.license',
-        ],
+    install_requires = requires,
 
-    entry_points = { },
+    extras_require = {
+        'fcgi': ['flup'],
+        },
 
+    entry_points = {
+        'console_scripts' : [
+            'server = cc.api.server:serve',
+            'noop = cc.api.server:noop',
+            'api.fcgi = cc.api.server:fcgi',
+            ],
+        'paste.app_factory': [
+            'api=cc.api.server:app_factory',
+            ],
+        },
+
+    test_suite = 'nose.collector',
+    
     # author metadata
     author = 'John E Doig III',
     author_email = 'john at creativecommons.org',
     description = 'Creative Commons REST API web service.',
     license = 'MIT',
+    url = 'http://api.creativecommons.org',
 
     )


hooks/post-receive
-- 
cc.api based on sane cc.license



More information about the cc-commits mailing list