Flask-Restx Api.model(strict=True) allowing unspecified params - python

I have a simple Users resource with a put method to update all user information except user password. According to Flask-Restx docs when a model has set the strict and validation params to true, a validation error will be thrown if an unspecified param is provided in the request. However, this doesn't seem to be working for me.
Model definition:
from flask_restx import Namespace, Resource, fields, marshal
users_ns = Namespace("users")
user = users_ns.model(
"user",
{
"user_name": fields.String(example="some_user", required=True),
"email": fields.String(example="some.user#email", required=True),
"is_admin": fields.Boolean(example="False"),
"is_deactivated": fields.Boolean(example="False"),
"created_date": fields.DateTime(example="2020-12-01T01:59:39.297904"),
"last_modified_date": fields.DateTime(example="2020-12-01T01:59:39.297904"),
"uri": fields.Url("api.user"),
},
strict=True,
)
user_post = users_ns.inherit(
"user_post", user, {"password": fields.String(required=True)}
) # Used for when
Resource and method definition:
from api.models import Users
class User(Resource):
#users_ns.marshal_with(user)
#users_ns.expect(user, validate=True)
def put(self, id):
"""
Update a specified user.
"""
user = Users.query.get_or_404(id)
body = request.get_json()
user.update(body)
return user
Failing Test:
def test_update_user_invalid_password_param(self, client, db):
""" User endpoint should return 400 when user attempts to pass password param to update. """
data = {
"user_name": "some_user",
"email": "some.user#email.com",
"password": "newpassword",
}
response = client.put(url_for("api.user", id=1), json=data)
assert response.status_code == 400
The response.status_code here is 200 because no validation error is thrown for the unspecified param passed in the body of the request.
Am I using the strict param improperly? Am I misunderstanding the behavior of strict?
UPDATED: I've added the test for strict model param from Flask-RestX repo (can be found here) for more context on expected behavior:
def test_api_payload_strict_verification(self, app, client):
api = restx.Api(app, validate=True)
ns = restx.Namespace("apples")
api.add_namespace(ns)
fields = ns.model(
"Person",
{
"name": restx.fields.String(required=True),
"age": restx.fields.Integer,
"birthdate": restx.fields.DateTime,
},
strict=True,
)
#ns.route("/validation/")
class Payload(restx.Resource):
payload = None
#ns.expect(fields)
def post(self):
Payload.payload = ns.payload
return {}
data = {
"name": "John Doe",
"agge": 15, # typo
}
resp = client.post_json("/apples/validation/", data, status=400)
assert re.match("Additional properties are not allowed \(u*'agge' was unexpected\)", resp["errors"][""])

I resolved my issue by pulling the latest version of Flask-RESTX from Github. The strict parameter for models was merged after Flask-RESTX version 0.2.0 was released on Pypi in March of 2020 (see the closed issue in Flask-RESTX repo for more context). My confusion arose because the documentation appears to represent the latest state of master and not the last Pypi release.

It's been a while since I touched on this but from what I can tell, I don't think you are using the strict param correctly. From the documentation here, the :param bool strict is defined as
:param bool strict: validation should raise an error when there is param not provided in schema
But in your last snippet of code, you are trying to validate with the dictionary data with the body of the request.
If I recall well again, for this sort of task you need to use (and as you mentioned) a RequestParser. There's a good example of it here - flask - something more strict than #api.expect for input data?

Related

How to process Ajax Data in Python (Django)

I want to push front end data (Form inputs) to the server via Ajax. For this, I created an Ajax post request but I'm very unsteady...
At my first attemps, I constantly receive errors by python
Ajax call:
//Get journey time for the stated address
jQuery.ajax({
type: 'post',
url: 'http://127.0.0.1:8000/termin/get-journey-time/',
data: {
'method': 'get_journey_time',
'mandant_id': 1,
'customer_address': customer_address,
'staff_group': staff_group_id
},
error: function (jqXHR, textStatus, errorThrown) {
console.log("Error")
},
timeout: 120000,
});
I've created a view in Python, in which I want to do something (views.py)
class get_journey_time(generics.ListAPIView):
"""
Handle Ajax Post to calculate the journey time to customer for the selected staff group
"""
permission_classes = (AllowAny,)
def post(self, request, *args, **kwargs):
print(request)
In my url route file I have this code lines (urls.py)
urlpatterns = [
XXXXXXXXXXXXXXXXXXXXXXXXX,
path('termin/get-journey-time/', views.get_journey_time.as_view()),
XXXXXXXXXXXXXXXXXXXXXXXXX,
XXXXXXXXXXXXXXXXXXXXXXXXX,
]
I got the Error code 500:
Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from the view, but received a `<class 'NoneType'>`
Is there a mistake in my approach, did I miss anything or is it completely crap?
Define renderer classes and parser classes in settings.py.
Note: You can define many of these (based on requirements and needs) but here we only need JSON related.
As a reference, you can check my repo's this file https://github.com/hygull/p-host/blob/master/src/pmt_hostel_app/views.py. I have used function based views, just ignore the code inside it and focus on request.data and also check related HTML files.
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
)
}
In this way you will be able to access the posted data in the form of dictionary which can be obtained using request.data in views.
Finally, return Response from the post() method. By default return type of function/method is None and you are just printing the request.
Check the below mentioned links, it will help you a lot.
https://www.django-rest-framework.org/api-guide/renderers/
https://www.django-rest-framework.org/api-guide/parsers/
In client code, I mean in JavaScript code, define a success callback as well (you have just defined error callback).
Please comment, if you are stuck.
you can do it like this
from rest_framework.response import Response
from rest_framework.views import APIView
class get_journey_time(APIView):
# ListAPIView is used for read-only endpoints
#
"""
Handle Ajax Post to calculate the journey time to customer for the selected staff group
"""
permission_classes = (AllowAny,)
def post(self, request, *args, **kwargs):
# you can get the posted data by request.data
posted_data = request.data
data = {"test": "test"}
return Response(data)
you can get the posted data and use serializers. you can start learning playing with serializers from here
Example serializer code can be like this
from rest_framework import serializers
class DummySerializer(serializers.Serializer):
name = serializers.CharField()
mobile_number = serializers.CharField(required=False)
and then use it in post method of your get_journey_time class

How to add custom error codes to Django Rest Framework

I am putting together an API with Django Rest Framework. I want to customise my error handling. I read quite a bit (link1, link2, link3) about custom error handling but can't find something that suits my needs.
Basically, I'd like to change the structure of my error messages to get something like this :
{
"error": True,
"errors": [
{
"message": "Field %s does not exist",
"code": 1050
}
]
}
Instead of :
{"detail":"Field does not exist"}
I already have a custom ExceptionMiddleware to catch the 500 errors and return a JSON, but I have no power on all the other errors.
Code of the ExceptionMiddleware:
class ExceptionMiddleware(object):
def process_exception(self, request, exception):
if request.user.is_staff:
detail = exception.message
else:
detail = 'Something went wrong, please contact a staff member.'
return HttpResponse('{"detail":"%s"}'%detail, content_type="application/json", status=500)
From Django doc :
Note that the exception handler will only be called for responses
generated by raised exceptions. It will not be used for any responses
returned directly by the view, such as the HTTP_400_BAD_REQUEST
responses that are returned by the generic views when serializer
validation fails.
This is exactly what I am trying to achieve, customise those 400 errors.
Thanks a lot,
I know this is a bit late, (better late than never).
If you have a structured error message, then try this by inheriting the Exception class
from rest_framework.serializers import ValidationError
from rest_framework import status
class CustomAPIException(ValidationError):
status_code = status.HTTP_400_BAD_REQUEST
default_code = 'error'
def __init__(self, detail, status_code=None):
self.detail = detail
if status_code is not None:
self.status_code = status_code
and the usage will be like this:
if some_condition:
error_msg = {
"error": True,
"errors": [
{
"message": "Field %s does not exist"%('my_test_field'),
"code": 1050
}
]
}
raise CustomAPIException(error_msg)
Reference : How to override exception messages in django rest framework
This is my custom exception handler:
def api_exception_handler(exception, context):
if isinstance(exception, exceptions.APIException):
headers = {}
if getattr(exception, 'auth_header', None):
headers['WWW-Authenticate'] = exception.auth_header
if getattr(exception, 'wait', None):
headers['Retry-After'] = '%d' % exception.wait
data = exception.get_full_details()
set_rollback()
return Response(data, status=exception.status_code, headers=headers)
return exception_handler(exception, context)
It represents APIException errors in a format like this:
{
"field_name": [
{
"message": "Error message",
"code": "error_code"
},
{
"message": "Another error",
"code": "another_error"
}
]
}
Django Rest Framework reference documentation:
http://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
The exception handler is indeed what you're looking for. The current mixing do raise an exception in case of failed validation (https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py).
Note that the exception handler will only be called for responses generated by raised exceptions. It will not be used for any responses returned directly by the view, such as the HTTP_400_BAD_REQUEST responses that are returned by the generic views when serializer validation fails.
I think this part doesn't hold any longer and should be rephrased by removing the "generic" word.
Alright so based on Linovia's answer, here is my custom handler :
def custom_exception_handler(exc, context):
response = exception_handler(exc, context)
if response is not None:
errors = []
for msg in response.data.values():
errors.append({'message': msg[0], 'error': get_error_message()})
response.data = {"errors": errors}
return response

Django REST Framework's APIClient sends None as 'None'

In my tests, I send mock data of models that I've passed through the serializer. The serializer.data looks something like this
{
"field": None
}
However, the data that my API receives is formatted like
{
"field": "None"
}
which is a problem because I'm trying to specify a foreign key that is allowed to be null. Shouldn't the APIClient convert None into null instead of unicode?
Is there any way to fix this or get around it?
Here's my serializer
class MyModelSerializer(serializers.ModelSerializer):
field = serializers.PrimaryKeyRelatedField(
queryset=OtherModel.objects.all(), required=False, allow_null=True)
And my create method in a viewset
def create(self, request):
model = MyModel()
serializer = MyModelSerializer(model, data=request.data)
if serializer.is_valid():
serializer.save(owner=request.user)
return Response(serializer.data, status=201)
return Response(serializer.errors, status=406)
Also my model class
class MyModel(models.Model):
field= models.OneToOneField(
OtherModel, blank=True, null=True)
In addition to what Kevin already said, you can force the APIClient to send JSON using the parameter format='json'.
See the documentation.
The problem here is that the APIClient is sending data to the view as form-data by default, which doesn't have a concept of None or null, so it is converted to the unicode string None.
The good news is that Django REST framework will coerce a blank string to None for relational fields for this very reason. Alternatively, you can use JSON and actually send None or null, which should work without issues.
In addition to existing answers,
if you are expecting a null, this probably means you expect your api to receive json.
If that's the case, you may want to configure the test request default format to json instead of form-data:
In your setting.py:
REST_FRAMEWORK = {
...
'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}
This way, no need to add format='json' to each request

RESTfully routing an API based on user roles

I am developing an API using Flask-RESTful, and my application has three roles.
site_admin
department_admin
basic
For any given resource, the JSON object returned has a different set of keys based on each role.
For example, if you hit /orders as "site_admin", the result could look like this:
{
"orders": [
{"id": 1, "user": "foo", "paid": True, "department": "A", "code": 456},
{"id": 2, "user": "bar", "paid": False, "department": "A", "code": 567},
{"id": 3, "user": "meh", "paid": False, "department": "B", "code": 678}
]
}
However, if you hit /orders as "department_admin", the result could look like this:
{
"orders": [
{"id": 3, "user": "meh", "paid": False}
]
}
And if you hit /orders as "basic" it would be a very minimal JSON response like this:
{
"orders": [
{"id": 2, "paid": True}
]
}
What is the RESTful way of implementing this?
I can come up with three ways of doing it.
(1) using a request arg and filtering on that:
class Orders(restful.Resource):
def get(self):
if request.args['role'] == 'site_admin':
return admin_JSON_response()
elif request.args['role'] == 'department_admin':
return dept_admin_JSON_response()
else:
return basic_JSON_response()
api.add_resource(Orders, '/orders')
(2) filtering on the session object:
class Orders(restful.Resource):
def get(self):
if session['role'] == 'site_admin':
return admin_JSON_response()
elif session['role'] == 'department_admin':
return dept_admin_JSON_response()
else:
return basic_JSON_response()
api.add_resource(Orders, '/orders')
(3) having a different route for each role:
class OrdersSiteAdmin(restful.Resource):
def get(self):
return admin_JSON_response()
api.add_resource(OrdersSiteAdmin, '/orders_site_admin')
class OrdersDeptAdmin(restful.Resource):
def get(self):
return dept_admin_JSON_response()
api.add_resource(OrdersDeptAdmin, '/orders_dept_admin')
class OrdersBasic(restful.Resource):
def get(self):
return basic_JSON_response()
api.add_resource(OrdersBasic, '/orders_basic')
... Is there a consensus on which is the preferred way RESTfully?
Thanks so much!
Your option #2 violates the the "stateless" constraint, the use of user sessions is not a good idea in a REST API, and instead you should require your clients to provide authentication with every request.
Let's assume you fix #2 and instead of a user session you now have a current_user variable, which is populated during authentication. Then you could rewrite that example as follows:
class Orders(restful.Resource):
def get(self):
if current_user.role == 'site_admin':
return admin_JSON_response()
elif current_user.role == 'department_admin':
return dept_admin_JSON_response()
else:
return basic_JSON_response()
api.add_resource(Orders, '/orders')
Let's look at your three options one by one:
(1) specifies the role in the query string, which would enable any user to request any representation, just by passing the desired role. But why put the role in the query string? I assume you will authenticate your users, so knowing your user you also know the role. This seems unnecessary and will give you extra validation effort.
(3) creates different resources for each role. Once again, you have to ensure that a "basic" user does not have access to the two URLs that apply to the higher roles, so you have also some validation work here.
(2) assumes the user database stores the role of each user, so once the user is authenticated the correct representation for his/her role is returned based on the assigned role. This is, I think, the best option, as users have really no way to hack their way into data they are not allowed to see.
Speaking of being RESTful, I would also look at your representations, which can be improved. Consider implementing links to other resources instead of providing IDs, to comply with the HATEOAS constraint.

Swagger API documentation

I saw swagger documentation of Flask and Django. In Flask I can design and document my API hand-written.(Include which fields are required, optional etc. under parameters sections).
Here's how we do in Flask
class Todo(Resource):
"Describing elephants"
#swagger.operation(
notes='some really good notes',
responseClass=ModelClass.__name__,
nickname='upload',
parameters=[
{
"name": "body",
"description": "blueprint object that needs to be added. YAML.",
"required": True,
"allowMultiple": False,
"dataType": ModelClass2.__name__,
"paramType": "body"
}
],
responseMessages=[
{
"code": 201,
"message": "Created. The URL of the created blueprint should be in the Location header"
},
{
"code": 405,
"message": "Invalid input"
}
]
)
I can chose which parameters to include, and which not. But how do I implement the same in Django? Django-Swagger Document in
not good at all. My main issue is how do I write my raw-json in Django.
In Django it automates it which does not allows me to customize my json. How do I implement the same kind of thing on Django?
Here is models.py file
class Controller(models.Model):
id = models.IntegerField(primary_key = True)
name = models.CharField(max_length = 255, unique = True)
ip = models.CharField(max_length = 255, unique = True)
installation_id = models.ForeignKey('Installation')
serializers.py
class ActionSerializer(serializers.ModelSerializer):
class Meta:
model = Controller
fields = ('installation',)
urls.py
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
from modules.actions import views as views
urlpatterns = patterns('',
url(r'(?P<installation>[0-9]+)', views.ApiActions.as_view()),
)
views.py
class ApiActions(APIView):
"""
Returns controllers List
"""
model = Controller
serializer_class = ActionSerializer
def get(self, request, installation,format=None):
controllers = Controller.objects.get(installation_id = installation)
serializer = ActionSerializer(controllers)
return Response(serializer.data)
My questions are
1) If I need to add a field say xyz, which is not in my models how do I add it?
2) Quiet similar to 1st, If i need to add a field which accepts values b/w 3 provided values,ie a dropdown. how do I add it?
3) How I add an optional field? (since in case of PUT request, I might only update 1 field and rest leave it blank, which means optional field).
4) Also how do I add a field that accepts the json string, as this api does?
Thanks
I can do all of these things in Flask by hardcoding my api. But in Django, it automates from my models, which does not(as I believe) gives me the access to customize my api. In Flask, I just need to write my API with hands and then integrate with the Swagger. Does this same thing exist in Django?
Like I just need to add the following json in my Flask code and it will answer all my questions.
# Swagger json:
"models": {
"TodoItemWithArgs": {
"description": "A description...",
"id": "TodoItem",
"properties": {
"arg1": { # I can add any number of arguments I want as per my requirements.
"type": "string"
},
"arg2": {
"type": "string"
},
"arg3": {
"default": "123",
"type": "string"
}
},
"required": [
"arg1",
"arg2" # arg3 is not mentioned and hence 'opional'
]
},
Would this work:
class TriggerView(APIView):
"""
This text is the description for this API
mykey -- My Key parameter
"""
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAuthenticated,)
def post(self, request, format=None):
print request.DATA
return Response(status=status.HTTP_202_ACCEPTED)
the POST request:
Authorization:Basic YWRtaW46cGFzcw==
Content-Type:application/json
{"mykey": "myvalue"}

Categories