In my Flask-RESTful API, imagine I have two objects, users and cities. It is a 1-to-many relationship. Now when I create my API and add resources to it, all I can seem to do is map very easy and general URLs to them. Here is the code (with useless stuff not included):
class UserAPI(Resource): # The API class that handles a single user
def __init__(self):
# Initialize
def get(self, id):
# GET requests
def put(self, id):
# PUT requests
def delete(self, id):
# DELETE requests
class UserListAPI(Resource): # The API class that handles the whole group of Users
def __init__(self):
def get(self):
def post(self):
api.add_resource(UserAPI, '/api/user/<int:id>', endpoint='user')
api.add_resource(UserListAPI, '/api/users/', endpoint='users')
class CityAPI(Resource):
def __init__(self):
def get(self, id):
def put(self, id):
def delete(self, id):
class CityListAPI(Resource):
def __init__(self):
def get(self):
def post(self):
api.add_resource(CityListAPI, '/api/cities/', endpoint='cities')
api.add_resource(CityAPI, '/api/city/<int:id>', endpoint='city')
As you can see, I can do everything I want to implement a very basic functionality. I can GET, POST, PUT, and DELETE both objects. However, my goal is two-fold:
(1) To be able to request with other parameters like city name instead of just
city id. It would look something like:
api.add_resource(CityAPI, '/api/city/<string:name>', endpoint='city')
except it wouldn't throw me this error:
AssertionError: View function mapping is overwriting an existing
endpoint function
(2) To be able to combine the two Resources in a Request. Say I wanted to get all the
users associated with some city. In REST URLs, it should look something like:
/api/cities/<int:id>/users
How do I do that with Flask? What endpoint do I map it to?
Basically, I'm looking for ways to take my API from basic to useful.
Your are making two mistakes.
First, Flask-RESTful leads you to think that a resource is implemented with a single URL. In reality, you can have many different URLs that return resources of the same type. In Flask-RESTful you will need to create a different Resource subclass for each URL, but conceptually those URLs belong to the same resource. Note that you have, in fact, created two instances per resource already to handle the list and the individual requests.
The second mistake that you are making is that you expect the client to know all the URLs in your API. This is not a good way to build APIs, ideally the client only knows a few top-level URLs and then discovers the rest from data in the responses from the top-level ones.
In your API you may want to expose the /api/users and /api/cities as top-level APIs. The URLs to individual cities and users will be included in the responses. For example, if I invoke http://example.com/api/users to get the list of users I may get this response:
{
"users": [
{
"url": "http://example.com/api/user/1",
"name": "John Smith",
"city": "http://example.com/api/city/35"
},
{
"url": "http://example.com/api/user/2",
"name": "Susan Jones",
"city": "http://example.com/api/city/2"
}
]
}
Note that the JSON representation of a user includes the URL for that user, and also the URL for the city. The client does not need to know how to build these, because they are given to it.
Getting cities by their name
The URL for a city is /api/city/<id>, and the URL to get the complete list of cities is /api/cities, as you have it defined.
If you also need to search for cities by their name you can extend the "cities" endpoint to do that. For example, you could have URLs in the form /api/cities/<name> return the list of cities that match the search term given as <name>.
With Flask-RESTful you will need to define a new Resource subclass for that, for example:
class CitiesByNameAPI(Resource):
def __init__(self):
# ...
def get(self, name):
# ...
api.add_resource(CitiesByNameAPI, '/api/cities/<name>', endpoint = 'cities_by_name')
Getting all the users that belong to a city
When the client asks for a city it should get a response that includes a URL to get the users in that city. For example, let's say that from the /api/users response above I want to find out about the city of the first user. So now I send a request to http://example/api/city/35, and I get back the following JSON response:
{
"url": "http://example.com/api/city/35",
"name": "San Francisco",
"users": "http://example/com/api/city/35/users"
}
Now I have the city, and that gave me a URL that I can use to get all the users in that city.
Note that it does not matter that your URLs are ugly or hard to construct, because the client never needs to build most of these from scratch, it just gets them from the server. This also enables you to change the format of the URLs in the future.
To implement the URL that gets users by city you add yet another Resource subclass:
class UsersByCityAPI(Resource):
def __init__(self):
# ...
def get(self, id):
# ...
api.add_resource(UsersByCityAPI, '/api/cities/<int:id>/users', endpoint = 'users_by_city')
I hope this helps!
you can do the id/name thing without duplicating the resource:
api.add_resource(CitiesByNameAPI, '/api/cities/<name_or_id>', endpoint = 'cities_by_name')
class CitiesByNameAPI(Resource):
def get(self, name_or_id):
if name_or_id.isdigit():
city = CityModel.find_by_id(name_or_id)
else:
city = CityModel.find_by_name(name_or_id)
if city:
return city.to_json(), 200
return {'error': 'not found'}, 404
not sure if there are any negative effects from this.
Related
First of all, I'm very new to Django world, there could be a similar question, however i did not find a satisfactory answer.
Here is my scenario, i have few external REST endpoints, which I will hit from my Django app and get say 100-key JSON response. Now, when I'm writing my API in Django app, this response i'll have to trim and send it to outer world. Say for example,
My API is,
GET /api/profiles/1472
which will give user profile with id 1472. Now, this API will inturn call some other REST endpoint and fetch actual profile's data. So, in a way I'm writing a proxy endpoint. This proxy endpoint is supposed to trim out some fields and give it back to caller.
I've not written model classes for this.
What are best ways to achieve this in Django?
Edit 1:
Sample view will be like this,
class GetCompetitorProductsView(APIView):
"""
Get Competitor products view
"""
def post(self, request, format=None):
# I'll be having a list of fields to be trimmed from response.
# It will be separate for every API.
data = request.data
error_checks = system_errors.check_for_competitor_products_input_error(data)
if not error_checks:
response = call_to_rest(data)
return Response(response)
else :
return Response(error_checks, status = status.HTTP_412_PRECONDITION_FAILED)
And one more thing, same behavior is applied to all other APIs. So, I need more generic solution which can be easily applied to other APIs.
Basically this is how to filter in python
allowed_fields = ("first_name", "last_name", "email")
user_info = call_rest_endpoint(id=1472)
result = {key:value for key,value in user_info.items() if key in allowed_fields}
First line define what fields u want to return.
Second line call the endpoint and get the data from theird party API.
Third line consist of 3 statements.
user_info.items() convert dictionary into array key/values paris.
Build dictionary from these tuples
but only if the key was found in allowed_fields tuple
You can create function or mixin that you will put in parents of your view and then use it method for trimming. Here is example
class TrimDataMixin(object):
ALLOWED_FIELDS = None
def trim_data(self, data):
allowed_fields = self.ALLOWED_FIELDS or []
return {k: v for k, v in data.items() if k in allowed_fields}
class GetCompetitorProductsView(TrimDataMixin, APIView):
"""
Get Competitor products view
"""
ALLOWED_FIELDS = ['first_name', 'last_name']
def post(self, request, format=None):
# I'll be having a list of fields to be trimmed from response.
# It will be separate for every API.
data = request.data
error_checks = system_errors.check_for_competitor_products_input_error(data)
if not error_checks:
response = call_to_rest(data)
# trim data
response = self.trim_data(response)
return Response(response)
else:
return Response(error_checks, status = status.HTTP_412_PRECONDITION_FAILED)
I am creating this functionality with a Django application I have. I want to log all pages the user visits and display it to him.
I am using a middleware to achieve it.
class LoggingMiddleware:
"""
Class used to register and fetch pages where the user has been visiting.
"""
def process_template_response(self, request, response):
if request.user.is_authenticated():
UserHistory.objects.create(user=request.user, page_name=request.path, url=request.path)
if response.context_data:
response.context_data['user_history'] = UserHistory.objects.filter(user=request.user)
return response
I want to name these UserHistory entries in the database, instead of just set the url as the name (as it i s now).
I have though of adding a variable to all views I have, in a way that the request object has something like request.page_name.
Can someone think of a better way to do it?
I build web application with Django REST Framework. There is one simple view, which return reference Information with db fields.
resources.py:
RESOURCES = {
'genres': GenreSerializer(Genre.objects.all(), many=True).data,
'authors': AuthorSerializer(Author.objects.all(), many=True).data,
......
}
class ResourceApiView(views.APIView):
def get(self, request):
params = request.query_params
response_dict = {}
if params:
# Return RESOURSES based on query params
for i in params:
q = RESOURCES.get(i)
if q:
response_dict[i] = q
else:
# Return all RESOURSES
response_dict = RESOURCES
return Response(response_dict,
status=status.HTTP_200_OK
)
It works fine, but when I add new object to one the resources querysets. Nothing happens, it show old queries.
I tried printed RESOURSES in my module, it printed once and other get requests don't trigger it.
Then I move RESOURSES directly in class ResourceApiView and it's behavior same like when RESOURSES where in module.
class ResourceApiView(views.APIView):
RESOURCES = {
'genres': GenreSerializer(Genre.objects.all(), many=True).data,
'authors': AuthorSerializer(Author.objects.all(), many=True).data,
......
}
def get(self, request):
...
It work fine only when I put RESOURSES in get method.
class ResourceApiView(views.APIView):
def get(self, request):
RESOURCES = {
'genres': GenreSerializer(Genre.objects.all(), many=True).data,
'authors': AuthorSerializer(Author.objects.all(), many=True).data,
......
}
But why is it happening? Why I can't evaluate queries from class attributes for each method call?
this is more related to python than to django. Let's say you hava file lib.py
def say_hello():
print "hello"
GREETINGS = {
"hello": say_hello()
}
now go to another python file (or the shell) and just import your lib.py, you'll print "hello" to the console because when you import the file It starts resolving the code inside so it's creating the GREETINGS variable (RESOURCES in your case) and calling the say_hello() method, for you it's executing the query. However python is smart enough that if you import the file again he'll remember that you just imported it before so it wont load the module again, cause it already has the module reference saved.
Your query is beeing executed once when the view was first loaded, and reimporting the view wont make the reference change
The same for placing RESOURCES as a class attribute. The code was executed when the class was imported (again you can test it by creating a class on the lib.py example)
hope this clarifies :) but maybe the docs explains it better https://docs.python.org/2/tutorial/modules.html
Note:
I think that the .data on the serializer is actually executing the query. Without it your query and the serializer would just be stored as reference, because the ORM is lazy. Change your RESOURCES to improve the performance of your endpoint because right now if you request one single resource (e.g. 'authors) its still executing ALL the queries ('authors', 'genres', etc)
i am making an application with Tornado (using Tornado-JSON).
My goal is to make rest service that returns JSON and can just handle whatever is passed in parameters - search/find if you want (trying to feed emberjs with JSON for different parameters passed) for example:
class ServicesHandler(CorsMixin,APIHandler):
....
__url_names__ = ["services"]
class ServicesTenantHandler(ServicesHandler):
def get(self, tenant_id):
....
class ServicesIdHandler(ServicesHandler):
def get(self, id):
....
And this is what i get in routes for example above:
[
"/hostedservices/services/(?P<id>[a-zA-Z0-9_]+)/?$",
"<class 'sysinfo.hostedservices.ServicesIdHandler'>"
],
[
"/hostedservices/services/(?P<tenant_id>[a-zA-Z0-9_]+)/?$",
"<class 'sysinfo.hostedservices.ServicesTenantHandler'>"
]
However i cannot make it receive anything but /hostedservices/services/SOME_VALUE
and in this case everything is useless because the second class (ServiceIdHandler) is always invoked.
I am quite new to all python and tornado but shouldn't i be able (according to routes) to invoke in this way:
/hostedservices/services/?tenant_id=VALUE
or
/hostedservices/services/?id=value
surely i will have bunch of more attributes but somehow when i test it from soapUI it seems that i always have to pass 1 parameter as part of URL path (template style parameter) rather than regular parameter and because of that i cant make it to have 2 handlers with same attribute types (but different attributes)
i would appreciate all the help i can get at this point...
Thanks!
This routing should solve your problem:
[
"/hostedservices/services/tenant/(?P<id>[a-zA-Z0-9_]+)/?$",
"<class 'sysinfo.hostedservices.ServicesTenantHandler'>"
],
[
"/hostedservices/services/(?P<id>[a-zA-Z0-9_]+)/?$",
"<class 'sysinfo.hostedservices.ServicesIdHandler'>"
]
The route /hostedservices/services/tenant/VALUE will be generated for ServicesTenantHandler and the route /hostedservices/services/VALUE will be generated for ServicesIdHandler (instead of /hostedservices/services/?tenant_id=VALUE or /hostedservices/services/?id=value routes).
In order to obtain this result, the handlers must be:
class ServicesHandler(CorsMixin, APIHandler):
__url_names__ = ["services"]
....
class ServicesTenantHandler(ServicesHandler):
__url_names__ = ["services/tenant/"]
def get(self, id):
....
class ServicesIdHandler(ServicesHandler):
def get(self, id):
....
I'm using Django, and want to store data that is relevant only for the duration of a request, and not on the session.
Is it correct to add something to request.META, like:
request.META['acl'] = acl
In my situation, I am using Tastypie, with a custom authorization class, and need a way to pass data between functions... it seems like storing something on the request would be the right thing to do... I just don't know where to store such information. My class looks something like:
class MyAuthorization(Authorization):
def is_authorized(self, request, object=None):
acl = getMyAccessControlList(request.method,request.session['username'])
for permission in acl:
if permission in self.permissions[request.method]:
request.META['acl'] = acl
return True
return False
def apply_limits(self, request, object_class, rs):
if 'HAS_ALL_ACCESS' in request.META['acl']:
return rs
else if 'HAS_USER_ACCESS' in request.META['acl']:
rs = rs.filter(object_class.user==request.session['username'])
return rs
Futher, Tastypie creates a single REST resource object, with a single authorization class used by all threads, so it's not thread-safe to just put it on the authorization class.
UPDATE
As per Chris Pratt's feedback, no, it doesn't make sense to modify the request. Exploring further, it appears to be appropriate to modify the request initially through custom middleware, and then keep it constant for the rest of the request: https://docs.djangoproject.com/en/1.4/topics/http/middleware
In this case, the middleware will look something like:
class AccessControlListMiddleware(object):
def process_view(self,request,view_func,view_args,view_kwargs):
permissions = set()
for role in request.session['permissions']:
for permission in PERMISSION_LIST[request.method][role]:
permissions.add(permission)
request.acl = list(permissions)
No. Don't mess with the request object. Especially since these are methods on the same class, you should simply assign data to self:
self.acl = getMyAccessControlList(request.method,request.session['username'])
...
if 'HAS_ALL_ACCESS' in self.acl: