Flask: Decorator to verify JSON and JSON Schema - python

I have a flask application with calls expecting JSON payload. Before each call is processed, I have a 2-step error checking process:
Assert that the payload is a valid JSON
Assert that the JSON payload complies with a specific schema
Which is implemented in the following fashion:
#app.route('/activate', methods=['POST'])
def activate():
request_id = request.__hash__()
# Assert that the payload is a valid JSON
try:
input = request.json
except BadRequest, e:
msg = "payload must be a valid json"
return jsonify({"error": msg}), 400
# JSON Schema Validation
try:
validate(request.json, app.config['activate_schema'])
except ValidationError, e:
return jsonify({"error": e.message}), 400
Since this code is duplicated over many calls, I wonder If I can elegantly move it to a decorator, something in the formof:
#validate_json
#validate_schema(schema=app.config['activate_schema'])
#app.route('/activate', methods=['POST'])
def activate():
....
The problem is that the request argument is implicit: I can refer to it within the function, but it is not a parameter to it. Therefore, I am not sure how to use it within the decorator.
How can I implement the validation checks using Python decorators?

Just use the request context global in your decorator. It is available during any request.
from functools import wraps
from flask import (
current_app,
jsonify,
request,
)
def validate_json(f):
#wraps(f)
def wrapper(*args, **kw):
try:
request.json
except BadRequest, e:
msg = "payload must be a valid json"
return jsonify({"error": msg}), 400
return f(*args, **kw)
return wrapper
def validate_schema(schema_name):
def decorator(f):
#wraps(f)
def wrapper(*args, **kw):
try:
validate(request.json, current_app.config[schema_name])
except ValidationError, e:
return jsonify({"error": e.message}), 400
return f(*args, **kw)
return wrapper
return decorator
Apply these decorators before applying the #route decorator; you want to register the wrapped function, not the original function for the route:
#app.route('/activate', methods=['POST'])
#validate_json
#validate_schema('activate_schema')
def activate():
input = request.json

now you can use #expect_json directly
For Example
from flask import Flask, jsonify, g, url_for
from flask_expects_json import expects_json
# example imports
from models import User
from orm import NotUniqueError
app = Flask(__name__)
schema = {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'email': {'type': 'string'},
'password': {'type': 'string'}
},
'required': ['email', 'password']
}
#app.route('/register', methods=['POST'])
#expects_json(schema)
def register():
# if payload is invalid, request will be aborted with error code 400
# if payload is valid it is stored in g.data
# do something with your data
user = User().from_dict(g.data)
try:
user.save()
except NotUniqueError as e:
# exception path: duplicate database entry
return jsonify(dict(message=e.message)), 409
# happy path: json response
resp = jsonify(dict(auth_token=user.encode_auth_token(), user=user.to_dict()})
resp.headers['Location'] = url_for('users.get_user', user_id=user.id)
return resp, 201
or
from flask import Flask
from flask_expects_json import expects_json
app = Flask(__name__)
schema = {
'type': 'object',
'properties': {
'name': {'type': 'string', "minLength": 4, "maxLength": 15},
'mobile': {'type': 'string', "pattern": "^[1-9]{1}[0-9]{9}$"},
'email': {'type': 'string', "pattern": "[^#]+#[^#]+\.[^#]"},
'password': {'type': 'string', "pattern": "^.*(?=.{8,})(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!##$%^&+=]).*$"}
},
'required': ['name', 'mobile', 'email', 'password']
}
#app.route('/', methods=['POST'])
#expects_json(schema)
def index():
values = request.get_json()
print(values)
return values
get more from here

A late answer, but you're probably looking for something like marshmallow (flask-marshmallow) or toastedmarshmallow.

Related

Create a subscription returns error Notification endpoint must respond with 200 OK to validation request

I've been trying to create a subscription to a user's outlook mail inbox using Microsoft Graph.
I've defined all the necessary grants/rights on Azure AD. (Mail.Read, Mail.ReadWrite)
My create_subscription method is defined below:
def create_subscription(
self,
change_type: str,
notification_url: str,
resource: str,
expiration_datetime: datetime,
client_state: str = None,
**kwargs,
) -> Response:
if isinstance(expiration_datetime, datetime):
expiration_datetime = format_time(expiration_datetime, is_webhook=True)
data = {
"changeType": change_type,
"notificationUrl": notification_url,
"resource": resource,
"expirationDateTime": expiration_datetime,
"clientState": client_state,
}
data.update(kwargs)
response = self._client._post(self._client.base_url + "subscriptions", json=data)
return response
The _post call redirects it to the _post() function:
def _post(self, url, **kwargs):
return self._request("POST", url, **kwargs)
def _request(self, method, url, headers=None, **kwargs) -> Response:
_headers = {
"Accept": "application/json",
}
_headers["Authorization"] = "Bearer " + self.token["access_token"]
if headers:
_headers.update(headers)
if "Content-Type" not in _headers:
_headers["Content-Type"] = "application/json"
return self._parse(requests.request(method, url, headers=_headers, **kwargs))
When I call the create_subscription function:
client.webhooks.create_subscription( "created",
"https://****-**-***-**-***.in.ngrok.io/notification/listen",
"/me/mailfolders('inbox')/messages",
"2022-10-28T10:00:00.0000000Z", "SecretClientState" )
It throws the following error:
microsoftgraph.exceptions.BadRequest:
{
'error': {
'code': 'InvalidRequest',
'message': 'Subscription validation request failed. Notification endpoint must respond with 200 OK to validation request.',
'innerError': {
'date': '2022-10-25T05:30:53',
'request-id': '53763195-88e8-441f-b3e0-1520587f6ba1',
'client-request-id': '53763195-88e8-441f-b3e0-1520587f6ba1'
}
}
}
How would I be required to go about validating the subscription; because as per my understanding, when the subscription is created, my notification URL should respond with a '200 OK' status_code, along with the value of the validationtoken query string parameter as a plain-text response.
Would really appreciate it if someone could guide me through the entire process. Thanks!

Pass python list into json

I have a list in python (Django) that I'd like to pass as a json response. But when I console log the data out I get an empty array called num.
numbers = [10,15,20]
def get_data(request, *args,**kwargs):
data = {
'num': numbers,
}
return JsonResponse(data)
In flask I would use jsonify:
from flask import jsonify
def get_data(request, *args,**kwargs):
data = {
'num': numbers,
}
return jsonify(data)

python api common response format

I have a dictionary which I used to return my response in an API.
It is formatted as this:
response_data = {
'url': '',
'status': '',
'data': ''
}
when I update it inside my function I have to declare it in every function then update a data individually like:
response_data ['url'] = url
response_data ['status'] = status
response_data ['data'] = data
then return it as:
return response_data
Is there a way to not initiate it in every function I made, like a class or function which I can just easily pass the data or edit the data to be passed.
Have tried this, but you have to manually edit the data like what I was doing.
class Response:
response_data = {
'url': '',
'status': '',
'data': ''
}
def set_message(self, key, value):
try:
self.response_data[key] = value
except Exception as e:
raise Exception(e)
response = Response()
response.set_message('success', success)

Flask-RESTful - don't return object property instead of returning null

Let's say I've got a clients table with id, name and email fields. An email field is optional.
The code looks like this:
client_fields = {
'id' : fields.String,
'name' : fields.String,
'email' : fields.String
}
And for displaying:
class ClientList(Resource):
#marshal_with(client_fields)
def get(self):
return model.Client.query.all()
When email is not provided, API returns JSON like this:
{
"id": "1",
"name": "John Doe",
"email": null
}
But instead I want it to return this object:
{
"id": "1",
"name": "John Doe"
}
Which basically means that instead of a property with null value I want it to return no such property at all. Is there a way to achieve that?
I would use the marshal function instead of the marshal_with decorator:
class ClientList(Resource):
def get(self):
clients = []
for client in model.Client.query.all():
if client.email:
clients.append(marshal(client_fields))
else:
clients.append(marshal(client_fields_no_email))
return clients
Or even better
class ClientList(Resource):
def get(self):
return [client_marshal(client) for client in model.Client.query.all()]
with
def client_marshal(client):
if client.email:
return {'id' : fields.String,
'name' : fields.String,
'email' : fields.String}
else:
return {'id' : fields.String,
'name' : fields.String}
There are two ways it can be done, pre-marshalling and post-marshalling modification. Pre-marshalling removes any default values given to field names in the client_fields dict but post-marshalling preserves them.
In pre-marshalling method, you have to pass a modified fields dict to marshal function if client's email is None.
For example;
import json
from flask_restful import fields, marshal, marshal_with
class Client(object):
def __init__(self, id, name, email=None):
self.id = id
self.name = name
self.email = email
client_fields = {
'id': fields.String,
'name': fields.String,
'email': fields.String
}
def get():
clients =[Client(1, 'Tom'), Client(2, 'John', 'john#example.com')]
return [marshal(client, client_fields if client.email else {k: v for k, v in client_fields.items() if k != 'email'}) for client in clients]
print(json.dumps(get()))
Output;
[{"id": "1", "name": "Tom"}, {"email": "john#example.com", "id": "2", "name": "John"}]
In post-marshalling you have to remove the email field of the OrderedDict returned by marshal_with if it is None.
The de_none function by default removes all fields those are None or you have to explicitly pass the field names if that's not desired and you have to pass envelope argument too if marshal_with takes the same.
from functools import wraps
import json
from flask_restful import fields, marshal, marshal_with
client_fields = {
'id': fields.String,
'name': fields.String,
#'email': fields.String(default='user#example.com')
'email': fields.String
}
class Client(object):
def __init__(self, id, name, email=None):
self.id = id
self.name = name
self.email = email
def de_none(envelope=None, *fields):
def decorator(func):
def dict_remove(d):
if fields:
for field in fields:
if d[field] is None:
d.pop(field)
else:
for k, v in d.items():
if v is None:
d.pop(k)
#wraps(func)
def decorated(*args, **kwargs):
data = result = func(*args, **kwargs)
if isinstance(result, tuple):
data = result[0]
if envelope:
data = data[envelope]
if isinstance(data, (list, tuple)):
for d in data:
dict_remove(d)
else:
dict_remove(data)
return result
return decorated
return decorator
#de_none()
#marshal_with(client_fields)
def get():
#return [Client(1, 'Tom'), Client(2, 'john', 'john#example.com')], 200, {'Etag': 'blah'}
#return [Client(1, 'Tom'), Client(2, 'john', 'john#example.com')]
#return Client(1, 'Tom'), 200, {'Etag': 'foo'}
return Client(1, 'Tom')
print(json.dumps(get()))
#de_none()
#marshal_with(client_fields)
def get():
return Client(2, 'John', 'john#example.com'), 201, {'Etag': 'ok'}
print(json.dumps(get()))
Output;
{"id": "1", "name": "Tom"}
{"email": "john#example.com", "id": "2", "name": "John"}
UPDATE Request hooks
The app.after_request decorator can be used to modify response object. Any default values given to fields are preserved.
The remove_none_fields request hook takes fields parameter which can be None to remove all fields with None value or a list of field names to selectively remove.
import json
from flask import Flask, Response
from flask_restful import fields, marshal_with, Api, Resource
app = Flask(__name__)
api = Api(app)
class Client(object):
def __init__(self, id, name, email=None):
self.id = id
self.name = name
self.email = email
client_fields = {
'id': fields.String,
'name': fields.String,
'email': fields.String,
'age': fields.String
}
class ClientList(Resource):
#marshal_with(client_fields)
def get(self):
clients =[Client(1, 'Tom'), Client(2, 'John', 'john#example.com')]
return clients, 200
#app.after_request
def remove_none_fields(resp, fields=('email',)):
"""
removes all None fields
"""
if not 'application/json' in resp.content_type:
return resp
def dict_remove(d, fields):
if fields:
for field in fields:
if d[field] is None:
d.pop(field)
else:
for k, v in tuple(d.items()):
if v is None:
d.pop(k)
data = json.loads(resp.get_data())
if isinstance(data, list):
for obj in data:
dict_remove(obj, fields)
else:
dict_remove(data, fields)
resp.set_data(json.dumps(data, indent=1))
resp.content_length = resp.calculate_content_length()
return resp
api.add_resource(ClientList, '/')
if __name__ == '__main__':
app.run(debug=True)
Output;
[
{
"age": null,
"name": "Tom",
"id": "1"
},
{
"age": null,
"email": "john#example.com",
"name": "John",
"id": "2"
}
]
update patching flask_restful.marshal
I filter out None values in a genexp inside marshal function and replace flask_restful.marshal with marshal defined here.
from collections import OrderedDict
from flask import Flask
import flask_restful
from flask_restful import fields, marshal_with, Api, Resource
app = Flask(__name__)
api = Api(app)
class Client(object):
def __init__(self, id, name, email=None):
self.id = id
self.name = name
self.email = email
client_fields = {
'id': fields.String,
'name': fields.String,
'email': fields.String,
}
def marshal(data, fields, envelope=None):
def make(cls):
if isinstance(cls, type):
return cls()
return cls
if isinstance(data, (list, tuple)):
return (OrderedDict([(envelope, [marshal(d, fields) for d in data])])
if envelope else [marshal(d, fields) for d in data])
items = ((k, marshal(data, v) if isinstance(v, dict)
else make(v).output(k, data))
for k, v in fields.items())
#filtering None
items = ((k,v) for k, v in items if v is not None)
return OrderedDict([(envelope, OrderedDict(items))]) if envelope else OrderedDict(items)
flask_restful.marshal = marshal
class ClientList(Resource):
#marshal_with(client_fields)
def get(self):
clients =[Client(1, 'Tom'), Client(2, 'John', 'john#example.com')]
return clients, 200
api.add_resource(ClientList, '/')
if __name__ == '__main__':
app.run(debug=True)
Output;
[
{
"id": "1",
"name": "Tom"
},
{
"email": "john#example.com",
"id": "2",
"name": "John"
}
]
You should use the skip_none property of the #marshal decorator. Its much more convenient than the approaches suggested in the other answers.
#marshal(some_model, skip_none=True)
def get():
...
The documentation can be found here: https://flask-restplus.readthedocs.io/en/stable/marshalling.html
This is also possible using Flask Restx :)

How to parse the POST argument to a REST service?

It seems I have another JSON problem, this time when posting to the REST service.
I am using Flask-Restful.
api.add_resource(Records, '/rest/records/<string:email>/<string:password>/<string:last_sync_date>')
parser = reqparse.RequestParser()
parser.add_argument('record_date', type=str)
parser.add_argument('records', type=str)
parser.add_argument('rating', type=str)
parser.add_argument('notes', type=str)
class Records(Resource):
def post(self, email, password, last_sync_date):
args = parser.parse_args()
records = args['records'] # 'records' = None, but why?
return records, 201
Unit test:
resource_fields = {
'record_date': fields.String,
'rating': fields.Integer,
'notes': fields.String,
'last_updated': fields.DateTime,
}
records = {"records":[]}
records["records"].append(marshal(record1, resource_fields))
rv = self.app.post('/rest/records/{0}/{1}/{2}'.format(email, password, sync_date), data=json.dumps(records))
json.dumps(records) is:
str: {"records": [{"rating": 1, "notes": null, "last_updated": "Tue, 15 Oct 2013 15:52:44 -0000", "record_date": "2013-10-15 15:52:44.746815"}]}
Why is args['records'] None, where I am clearly sending it over the wire?
UPDATE:
Strange part is when I send a single object, its all dandy. So strange:
record = dict(record_date=record1.record_date, rating=record1.rating, notes=record1.notes, last_updated=record1.last_updated)
rv = self.app.post('/rest/records/{0}/{1}/{2}'.format(email, password, sync_date), data=record)
args:
{'records': None, 'notes': None, 'task': None, 'record_date': '2013-10-15 16:48:40.662744', 'rating': '1'}
I ended up raising this as an issue on flask-resful github and got this solution, which works for me. Credit goes to Doug Black.
reqparse doesn't really know how to handle JSON. To deal with JSON posts, you'll want to use the flask.request.json dict.
Here's an updated example for what you probably want:
from flask import Flask, request
from flask.ext import restful
class Records(restful.Resource):
def post(self, email, password, last_sync_date):
records = request.json['records']
return records, 201
app = Flask(__name__)
api = restful.Api(app)
api.add_resource(
Records,
'/rest/records/<string:email>/<string:password>/<string:last_sync_date>'
)
The docs on request.json are here.
You'll need to make sure you post with the content type header set to application/json so flask knows to populate the json dictionary.
self.app.post(
'/rest/records/{0}/{1}/{2}'.format(email, password, sync_date),
data=json.dumps(records),
headers={'Content-Type': 'application/json'
)
class Records(Resource):
parser = reqparse.RequestParser()
parser.add_argument('record_date', type=str)
parser.add_argument('records', type=str)
parser.add_argument('rating', type=str)
parser.add_argument('notes', type=str)
def post(self):
args = parser.parse_args()
records = args['records']
return records, 201
I hope it works.

Categories