Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""LICENSE
2Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
4This file is part of jerrycan.
6jerrycan is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 3 of the License, or
9(at your option) any later version.
11jerrycan is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
16You should have received a copy of the GNU General Public License
17along with jerrycan. If not, see <http://www.gnu.org/licenses/>.
18LICENSE"""
20import sentry_sdk
21from functools import wraps
22from typing import Callable
23from flask import jsonify, make_response, request
24from werkzeug.exceptions import Unauthorized
25from jerrycan.exceptions import ApiException
26from jerrycan.base import app
29def api(func: Callable) -> Callable:
30 """
31 Decorator that handles common API patterns and ensures that
32 the JSON response will always follow a certain pattern
33 :param func: The function to wrap
34 :return: The wrapper function
35 """
37 @wraps(func)
38 def wrapper(*args, **kwargs):
39 """
40 Tries running the function and checks for errors
41 :param args: args
42 :param kwargs: kwargs
43 :return: The JSON response including an appropriate HTTP status code
44 """
45 code = 200
46 response = {"status": "ok"}
48 try:
49 is_json = request.content_type is not None \
50 and request.content_type.startswith("application/json") \
51 and request.is_json \
52 and isinstance(request.get_json(silent=True), dict)
53 if request.method in ["POST", "PUT", "DELETE"] and not is_json:
54 raise ApiException("not in json format", 400)
56 try:
57 response["data"] = func(*args, **kwargs)
58 except ApiException as e:
59 if e.status_code >= 500:
60 app.logger.error(f"Caught exception in API: {e}")
61 sentry_sdk.capture_exception(e)
62 raise e
63 except (KeyError, TypeError, ValueError) as e:
64 raise e
65 except BaseException:
66 raise ApiException("server error", 500)
68 except (KeyError, TypeError, ValueError, ApiException) as e:
70 response["status"] = "error"
72 if isinstance(e, ApiException):
73 code = e.status_code
74 response["reason"] = e.reason
76 else:
77 code = 400
78 response["reason"] = "bad request: {}".format(type(e).__name__)
80 return make_response(jsonify(response), code)
81 return wrapper
84def api_login_required(func: Callable) -> Callable:
85 """
86 Decorator to make unauthorized API calls respond with JSON properly
87 :param func: The function to wrap
88 :return: The wrapped function
89 """
91 @wraps(func)
92 def wrapper(*args, **kwargs):
93 """
94 Checks if flask-login throws an Unauthorized exception. If so,
95 re-wrap the response in JSON
96 :param args: The function arguments
97 :param kwargs: The function keyword arguments
98 :return: The newly wrapped response,
99 or just the plain response if authorized
100 """
102 try:
103 resp = func(*args, **kwargs)
104 return resp
105 except Unauthorized:
106 return make_response(
107 jsonify({"status": "error", "reason": "unauthorized"}), 401
108 )
110 return wrapper