Simple RESTful API - make a patch

276 views
Skip to first unread message

Kearney Taaffe

unread,
Nov 10, 2016, 11:10:32 AM11/10/16
to cherrypy-users
I'm making an API in CherryPy. I must admit, it's super easy, and I'd like to turn this example into something that the CherryPy community puts on it's website as a "how to API"

The problem I'm having is how to handle an HTTP PATCH request. The following code fails with the error for HTTP PATCH requests

AttributeError: 'Request' object has no attribute 'json'

Here's my main.py
import cherrypy


from controllers.userController import UserController




def CORS():
   
"""Allow web apps not on the same server to use our API
    """

    cherrypy
.response.headers["Access-Control-Allow-Origin"] = "*"
    cherrypy
.response.headers["Access-Control-Allow-Headers"] = (
       
"content-type, Authorization, X-Requested-With"
   
)
   
    cherrypy
.response.headers["Access-Control-Allow-Methods"] = (
       
'GET, POST, PUT, DELETE, OPTIONS'
   
)
   
if __name__ == '__main__':
   
"""Starts a cherryPy server and listens for requests
    """

   
    userController
= UserController()
   
    cherrypy
.tools.CORS = cherrypy.Tool('before_handler', CORS)
   
    cherrypy
.config.update({
       
'server.socket_host': '0.0.0.0',
       
'server.socket_port': 8080,
       
'tools.CORS.on': True,
   
})


   
# API method dispatcher
   
# we are defining this here because we want to map the HTTP verb to
   
# the same method on the controller class. This _api_user_conf will
   
# be used on each route we want to be RESTful
    _api_conf
= {
       
'/': {
           
'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
       
}
   
}


   
# _api_user_conf better explained
   
# The default dispatcher in CherryPy stores the HTTP method name at
   
# :attr:`cherrypy.request.method<cherrypy._cprequest.Request.method>`.


   
# Because HTTP defines these invocation methods, the most direct
   
# way to implement REST using CherryPy is to utilize the
   
# :class:`MethodDispatcher<cherrypy._cpdispatch.MethodDispatcher>`
   
# instead of the default dispatcher. To enable
   
# the method dispatcher, add the
   
# following to your configuration for the root URI ("/")::


   
#     '/': {
   
#         'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
   
#     }


   
# Now, the REST methods will map directly to the same method names on
   
# your resources. That is, a GET method on a CherryPy class implements
   
# the HTTP GET on the resource represented by that class.


   
# http://cherrypy.readthedocs.org/en/3.2.6/_sources/progguide/REST.txt


    cherrypy
.tree.mount(userController, '/api/users', _api_conf)
   


    cherrypy
.engine.start()
    cherrypy
.engine.block()



Here's the user controller (controllers/userController.py) (NOTE: be sure the file __init__.py is in the controllers directory and is blank)

import cherrypy


# from services.userServiceProvider import UserServiceProvider


from typing import Dict, List


'''
NOTES
 + @cherrypy.tools.json_out() - automatically outputs response in JSON
 + @cherrypy.tools.json_in()  - automatically parses JSON body
'''

class UserController():


   
# expose all the class methods at once
    exposed
= True
   
   
def __init__(self):
       
# create an instance of the service provider
       
# self.userService = UserServiceProvider()
       
pass


   
'''
    This code allows for our routes to look like http://example.com/api/users/uuid
    and the uuid will be made available to the routes like the user input
    http://example.com/api/users?uuid=uuid
    '''

   
def _cp_dispatch(self, vpath: List[str]):
       
       
# since our routes will only contain the GUID, we'll only have 1
       
# path. If we have more, just ignore it
       
if len(vpath) == 1:
            cherrypy
.request.params['uuid'] = vpath.pop()
       
       
return self


   
@cherrypy.tools.json_out()
   
def GET(self, **kwargs: Dict[str, str]) -> str:
       
"""
        Either gets all the users or a particular user if ID was passed in.
        By using the cherrypy tools decorator we can automagically output JSON
        without having to using json.dumps()
        """



       
# our URI should be /api/users/{GUID}, by using _cp_dispatch, this
       
# changes the URI to look like /api/users?uuid={GUID}
       
       
if 'uuid' not in kwargs:
           
# if no GUID was passed in the URI, we should get all users' info
           
# from the database
           
# results =  self.userService.getAllUsers()
            results
= {
               
'status' : 'getting all users'
           
}
       
else:
           
# results = self.userService.getUser(kwargs['uuid'])
            results
= {
               
'status' : 'searching for user ' + kwargs['uuid']
           
}


       
return results
   
   
@cherrypy.tools.json_in()
   
@cherrypy.tools.json_out()
   
def POST(self):
       
"""Creates a new user
        """

        input
= cherrypy.request.json
        inputParams
= {}
       
       
# convert the keys from unicode to regular strings
       
for key, value in input.items():
           inputParams
[key] = str(value)
       
       
try:
           
# result = self.userService.addUser(inputParams)
            result
= {
               
'status' : 'inserting new record'
           
}


           
if len(inputParams) == 0:
               
raise Exception('no body')
       
except Exception as err:
            result
= {'error' : 'Failed to create user. ' + err.__str__()}


       
return result
   
   
@cherrypy.tools.json_out()
   
def DELETE(self, **kwargs: Dict[str, str]):
       
# convert the keys from unicode to regular strings
        uuid
= ''
       
if 'uuid' not in kwargs:
            result
= {
               
'success' : False,
               
'message' : 'You must specfy a user.'
           
}


           
return result


        uuid
= kwargs['uuid']


       
try:
           
if len(uuid) == 0:
               
raise Exception('must pass in user ID')
           
           
# result = self.userService.deleteUser(inputParams)
            result
= {
               
'status' : 'deleting user with ID: ' + uuid
           
}
       
except Exception as err:
            result
= {'error' : 'could not delete. ' + err.__str__()}


       
return result


   
@cherrypy.tools.json_in()
   
@cherrypy.tools.json_out()
   
def PUT(self):
       
# get the request body
        data
= cherrypy.request.json
       
print('BODY:\n' + str(data))
       
# result = self.userService.updateUser(data)
        result
= {
           
'status' : 'updating user'
       
}


       
return result


   
@cherrypy.tools.json_in()
   
@cherrypy.tools.json_out()
   
def PATCH(self, **kwargs: Dict[str, str]):


       
# the _cp_dispatch() method
       
if 'uuid' not in kwargs:
            result
= {
               
'success' : False,
               
'message' : 'You must specfy a user.'
           
}


           
return result
       
else:
           
print('found uuid: ' + kwargs['uuid'])


       
# get the request body
        data
= cherrypy.request.json
       
       
# result = self.userService.updateUser(data, kwargs['uuid'])
        result
= {
           
'status' : 'patching user ({})'.format(kwargs['uuid'])
       
}


       
return result


   
def OPTIONS(self):
       
return 'Allow: DELETE, GET, HEAD, OPTIONS, POST, PUT'


   
def HEAD(self):
       
return ''



By the way, feel to kang this code all you want. And, if I'm doing something wrong, please tell me.

Tim Roberts

unread,
Nov 10, 2016, 12:50:14 PM11/10/16
to cherryp...@googlegroups.com
Kearney Taaffe wrote:
I'm making an API in CherryPy. I must admit, it's super easy, and I'd like to turn this example into something that the CherryPy community puts on it's website as a "how to API"

The problem I'm having is how to handle an HTTP PATCH request. The following code fails with the error for HTTP PATCH requests

AttributeError: 'Request' object has no attribute 'json'

I've never used the JSON tool, but I can't help but notice this:


    cherrypy.response.headers["Access-Control-Allow-Methods"] = (
       
'GET, POST, PUT, DELETE, OPTIONS'
   
)
...

    def OPTIONS(self):
       
return 'Allow: DELETE, GET, HEAD, OPTIONS, POST, PUT'

Neither of those lists include PATCH.
-- 
Tim Roberts, ti...@probo.com
Providenza & Boekelheide, Inc.

Kearney Taaffe

unread,
Nov 10, 2016, 1:00:39 PM11/10/16
to cherrypy-users
@Tim, unfortunately, adding the PATCH to the response headers and the OPTIONS function didn't do anything. I'm still getting the error

line 143, in PATCH
data = cherrypy.request.json

Here's the data I'm sending in for the PATCH request:

PATCH /api/users/123-123-123 HTTP/1.1
Content-Type: application/json; charset=utf-8
Host: localhost:8080
Connection: close
User-Agent: Paw/3.0.12 (Macintosh; OS X/10.12.1) GCDHTTPRequest
Content-Length: 21


{"last_name":"Jones"}

Joseph S. Tate

unread,
Nov 10, 2016, 1:52:00 PM11/10/16
to cherryp...@googlegroups.com
When I try to reproduce via "telnet localhost 8080", the body line gets pasted to my prompt, so cherrypy is terminating the request before the body is sent. So it's likely a configuration error of some kind.

I'm going to pursue that for a bit, but wanted to report that before you go diving into something in the request handler itself.


On Thu, Nov 10, 2016 at 11:10 AM Kearney Taaffe 
<snipped>

Joseph S. Tate

unread,
Nov 10, 2016, 2:22:13 PM11/10/16
to cherryp...@googlegroups.com
It looks like there's at least one bug in CherryPy. _cprequest.py
needs to be updated to look like this: 

    methods_with_bodies = ('POST', 'PUT', 'PATCH')

But then I'm getting an error on the JSON document. I hope this is enough to help you get going.

If you have time to submit a pull request with this change (and hopefully a test), I'd appreciate it.

Kearney Taaffe

unread,
Nov 14, 2016, 8:40:13 AM11/14/16
to cherrypy-users
@Josheph

That fixed it!

I changed my def PATHCH method to the following:

    @cherrypy.tools.json_in()
   
@cherrypy.tools.json_out()
   
def PATCH(self, **kwargs: Dict[str, str]):


       
# the _cp_dispatch() method
       
if 'uuid' not in kwargs:
            result
= {
               
'success' : False,
               
'message' : 'You must specfy a user.'
           
}


           
return result
       
else:
           
print('found uuid: ' + kwargs['uuid'])


       
# get the request body
        data
= cherrypy.request.
json


       
print('HTTP BODY: ' + str(data))

       
       
# result = self.userService.updateUser(data, kwargs['uuid'])
        result
= {
           
'status' : 'patching user ({})'.format(kwargs['uuid'])
       
}


       
return result


And, that printed both the body and the route parameter!

So, what do I need to do next? Submit a bug report? Branch, and ask my branch to be merged into the master branch?

Thanks for all your help! I super appreciate it! 

Kearney Taaffe

unread,
Nov 14, 2016, 11:24:58 AM11/14/16
to cherrypy-users
@Josheph, sorry, my response wasn't very explicit. Changing the line 315 in cherrypy._cprequest.py from

methods_with_bodies = ('POST', 'PUT')


to

methods_with_bodies = ('POST', 'PUT', 'PATCH')


worked.

I posted the new PATCH() method to show that the body and route param were printed to the log

Joseph S. Tate

unread,
Nov 15, 2016, 1:44:11 AM11/15/16
to cherrypy-users
Ideally you'd submit a pull request from your branch with a test that proves that the fix works. Perhaps by copying and modifying a test for PUT or POST. Then I'll merge it in.

Joseph

--
You received this message because you are subscribed to the Google Groups "cherrypy-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cherrypy-user...@googlegroups.com.
To post to this group, send email to cherryp...@googlegroups.com.
Visit this group at https://groups.google.com/group/cherrypy-users.
For more options, visit https://groups.google.com/d/optout.

Kearney Taaffe

unread,
Nov 15, 2016, 8:15:28 AM11/15/16
to cherryp...@googlegroups.com
Man, I feel stupid. I work with Git at work, but, all we do is just branch and merge. I forked the project (since I couldn't seem to branch) and made a branch with the ticket number.


I don't know if you can check that out and merge it. I created a tests folder, with a simple server and controller, and a caller file that uses "requests" to send a PATCH request.




Kearney J. Taaffe

To unsubscribe from this group and stop receiving emails from it, send an email to cherrypy-users+unsubscribe@googlegroups.com.
To post to this group, send email to cherrypy-users@googlegroups.com.

--
You received this message because you are subscribed to a topic in the Google Groups "cherrypy-users" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/cherrypy-users/-HvSQC0Lmas/unsubscribe.
To unsubscribe from this group and all its topics, send an email to cherrypy-users+unsubscribe@googlegroups.com.
To post to this group, send email to cherrypy-users@googlegroups.com.

Joseph S. Tate

unread,
Nov 15, 2016, 5:55:42 PM11/15/16
to cherryp...@googlegroups.com
you're almost there. Go to https://github.com/cherrypy/cherrypy/ and click the "New Pull Request" button. Then click the "compare across forks" link. Then you can pick your repo and branch as the "head fork".

To unsubscribe from this group and stop receiving emails from it, send an email to cherrypy-user...@googlegroups.com.
To post to this group, send email to cherryp...@googlegroups.com.

--
You received this message because you are subscribed to a topic in the Google Groups "cherrypy-users" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/cherrypy-users/-HvSQC0Lmas/unsubscribe.
To unsubscribe from this group and all its topics, send an email to cherrypy-user...@googlegroups.com.

To post to this group, send email to cherryp...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "cherrypy-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cherrypy-user...@googlegroups.com.
To post to this group, send email to cherryp...@googlegroups.com.

Joseph S. Tate

unread,
Nov 15, 2016, 5:56:31 PM11/15/16
to cherryp...@googlegroups.com
Ahh. I see in the ticket that you're already working it. Thanks!

Tim Roberts

unread,
Nov 15, 2016, 8:12:26 PM11/15/16
to cherryp...@googlegroups.com
Kearney Taaffe wrote:

Man, I feel stupid. I work with Git at work, but, all we do is just branch and merge.

Don't feel stupid.  Git is approaching sendmail in scope and complexity.  Everything that is computable can be done with git, but someone has to tell you the spelling.

Kearney Taaffe

unread,
Nov 17, 2016, 12:26:50 PM11/17/16
to cherrypy-users
So, I FINALLY figured out how to create a test, and get it working :-D it took way to much time.

Can you take a look at my test to see if I'm doing it correctly? I'm unfamiliar with writing test cases, but I think the test is valid.

On and aside, I also found (from reading one of the test cases) that we can overwrite in the config the HTTP request types that accept a body

        appconf = {
           
'/method': {
               
'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
               
'request.methods_with_bodies': ('POST', 'PUT', 'PATCH')
           
},
       
}


        cherrypy
.tree.mount(root, config=appconf)


But, I still think the ticket is valid. HTTP PATCH requests should accept a body no matter what
Reply all
Reply to author
Forward
0 new messages