2

This is my first attempt at designing a web application. I have created a CRUD operating API for a SQLAlchemy database. I am now trying to design a RESTful web framework using Pyramid to support this DB and all its entities.

I have been struggling on how to PUT (update). I know there are certain conditions that I also need to incorporate (e.g. Code 202). I am uncertain on how to handle the object replacement using PUT. The method in the view configuration is a bit confusing and I believe I am following the db CRUD operations.

Secondly, I believe that all resources have to be looked up via {id}. I believe I have this correct in the get_user method. User_id holds all the information of the user in the db...so, if a user is looked up by id, than I shouldn't need to have all the other information (see delete_user method).

I have read some nifty tutorials on designing a RESTful design, but I am still struggling given that I am self taught. Your help is truly appreciated!

Here is an example of some code that shows the problem:

_ init _.py (URIs):

config.add_route('post_user', '/users') # POST (HTTP) / CREATE (CRUD)
config.add_route('get_user', '/users/{id}') #GET (HTTP) / RETRIEVE (CRUD)
config.add_route('put_user', '/users/{id}') # PUT (HTTP) / UPDATE (CRUD)
config.add_route('delete_user', '/users/{id}') # DELETE (HTTP) / DELETE (CRUD)

views.py (view config -- RESTful):

@view_defaults(renderer='json')
class RESTView(object):
    api = ConvenienceAPI() # CRUD API for database
    def __init__(self, request):
        self.request = request 

    @view_config(route_name='get_user', request_method='GET')
    def get_user(self):
        user_id = int(request.matchdict['user_id'])
        user = api.retrieve_user('user_id') 
        if user is None:
            raise HTTPNotFound()
        else:
            return user
    
    @view_config(route_name='post_user', request_method='POST')
    def post_user(self):
        username = request.matchdict['username']
        password = request.matchdict['password']
        firstname = request.matchdict['firstname']
        lastname = request.matchdict['lastname']
        email = request.matchdict['email']
        new_user = api.create_user('username', 'password', 'firstname', 'lastname', 'email')
        return Response{'new_user': user}
    
    @view_config(route_name='put_user', request_method='PUT')
    def put_user(self):
        user = request.matchdict['user_id']
        new_username = # is this pointing to another URL ???? 
        updated_user = api.update_user('username', 'new_username')
        return Response{status='202 Accepted'}
    
    @view_config(route_name='delete_user', request_method='DELETE')
    def delete_user(self):
        user = request.matchdict['user_id']
        del_user = api.delete_user('username', 'firstname', 'lastname')
        return Response{'del_user': user}
Community
  • 1
  • 1
thesayhey
  • 938
  • 3
  • 17
  • 38

1 Answers1

10

TLDR: You have lots of problems. Perhaps spend some more time learning python, then check out some blog posts on Pyramid and REST such as http://www.vargascarlos.com/2013/02/pyramid-and-rest/ or http://codelike.com/blog/2014/04/27/a-rest-api-with-python-and-pyramid/

You have some problems with your code, which I will address first. Then I will provide some advice for better arranging your views.

1) You have used {id} in the route config, thus you would access this via request.matchdict['id'], not request.matchdict['user_id'].

Bonus tip: If you want to force the ID to be numbers only, you can use {id:\d+}. The portion after the colon is regex. This should help defend against SQL injection as well.

2) Your view has defined itself to use the JSON renderer. This means your view function should return a python object (list, dict, etc), and pyramid will then give this object to the JSON renderer, which will use the data as it pleases, and create a response (in this case, turn it into valid json. In the case of a template renderer, it would use the object in the template). Thus, you dont need to return Response{}, but simply return {}. If you hadn't defined a renderer, then your view would have to make and return a response itself.

3) In your class methods such as get_user, you need to access the request with self.request, as you have saved the request to the class in __init__.

4) I take it you are new to python. In post_user for example, you have saved the username into the variable username with the line username = request.matchdict['username'] (which should be username = self.request.params['username']). When you have called your API though new_user = api.create_user('username', ..., you have passed in a string with the contents 'username', thus you have created a user whos name is 'username'. You actually want to pass in the variable username, eg new_user = api.create_user(username, ....

5) Further to #5, dicts in python are key:value. So in post_user, if you want to return the new user in the json under the key 'user', then you actually want return {'user': new_user}.

So now my main piece advice: you don't need to define a named route for each get/post/etc route, you can define 1 route, and then leverage view predicates to run the correct function.

You may also want to create one ConvenienceAPI() object, and then share it (depends exactly what that object is doing, and how it is functioning). You could even attach it to the request.

You should also look into re-using as many chunks of code as possible (ie the get_user function I included).

Here is what your code could look like, after taking on board all of the above;

config.add_route('users', '/users')
config.add_route('user', '/users/{id:\d+}')

API = ConvenienceAPI()

@view_defaults(route_name='users', renderer='json')
class UsersViews(object):

    api = API

    def __init__(self, request):
        self.request = request 

    @view_config(request_method='GET')
    def get(self):
        users = self.api.retrieve_users() 
        return users

    @view_config(request_method='POST')
    def post(self):
        username = self.request.POST.get('username')
        password = self.request.POST.get('password')
        firstname = self.request.POST.get('firstname')
        lastname = self.request.POST.get('lastname')
        email = self.request.POST.get('email')
        user = self.api.create_user(username, password, firstname, lastname, email)
        return user

@view_defaults(route_name='user', renderer='json')
class UserViews(object):

    api = API

    def __init__(self, request):
        self.request = request

    def get_user(self):
        user_id = int(self.request.matchdict['id'])
        user = self.api.retrieve_user(user_id)
        return user

    @view_config(request_method='GET')
    def get(self):
        user = self.get_user()
        if user is None:
            raise HTTPNotFound()
        return user

    @view_config(request_method='PUT')
    def put(self):
        user = self.get_user()
        if user is None:
            raise HTTPNotFound()
        new_username = self.request.POST.get('username')
        updated_user = self.api.update_user(user.username, new_username) # This is a strange way of updating the username, but your 'API' is not really relevant to this question.
        return HTTPAccepted() # Some REST services return the updated user.

    @view_config(request_method='DELETE')
    def delete(self):
        user = self.get_user()
        del_user = self.api.delete_user(user.username, user.firstname, user.lastname)
        if del_user: # If user was deleted.
            return HTTPAccepted() # Or something like this. As above, you might want to return the deleted user.
        else:
            return HTTPBadRequest()
neRok
  • 995
  • 9
  • 21
  • your advice is most helpful. Thank you for clearly explaining some of the issues and how I should frame working this code. The tidbits of advice are very handy. I am reworking my code and spending sometime with the blogs. (I think working a template for POST might assist me as well as I will be using a form in an html doc to enter in all the pieces of information.) Quick question: if I am using HTML templates, do I still use json? – thesayhey Sep 17 '15 at 14:21
  • For BEST PRACTICES, would you recommend making a view class per resource or put them all in one class? (e.g. `class UserViews`, `class VideoViews`, `class AssessmentViews`, etc...) – thesayhey Sep 17 '15 at 14:25
  • 1
    Re 3rd comment, my answer isn't meant to be 100% working solution, as I don't know how your API functions. Re 2nd comment, you could make a master class that has a model property and generic view functions, and then subclass the master for each specific class, and override the model property. This way the views could be common, but they operate against a different model. Re 1st comment, it depends on your website. If it's all server generated HTML, you might not need to use JSON at all. If it's a very javascript driven website, then you could POST the json form, rather than 'submitting' it. – neRok Sep 18 '15 at 01:07