[Advanced Python] Find exceptions and tell me in the right way
目标:一个 REST API,如何写异常,让client端使用的舒舒服服。
先来个热身
一、异常处理
Ref: Flask Rest API -Part:4- Exception Handling
需要单独写一个这样的文件:errors.py
#~/movie-bag/resources/errors.py class InternalServerError(Exception): pass class SchemaValidationError(Exception): pass class MovieAlreadyExistsError(Exception): pass class UpdatingMovieError(Exception): pass class DeletingMovieError(Exception): pass class MovieNotExistsError(Exception): pass class EmailAlreadyExistsError(Exception): pass class UnauthorizedError(Exception): pass errors = { "InternalServerError": { "message": "Something went wrong", "status": 500 }, "SchemaValidationError": { "message": "Request is missing required fields", "status": 400 }, "MovieAlreadyExistsError": { "message": "Movie with given name already exists", "status": 400 }, "UpdatingMovieError": { "message": "Updating movie added by other is forbidden", "status": 403 }, "DeletingMovieError": { "message": "Deleting movie added by other is forbidden", "status": 403 }, "MovieNotExistsError": { "message": "Movie with given id doesn't exists", "status": 400 }, "EmailAlreadyExistsError": { "message": "User with given email address already exists", "status": 400 }, "UnauthorizedError": { "message": "Invalid username or password", "status": 401 } }
Ref: Python异常编程技巧
-
异常自定义的必要性
自定义异常的必要性,至少自己看的清楚。
class Error(Exception): """Root exception for all exceptions raised by this module."""
--------------------------------
class SocketTimeError(Error): pass class SocketRefuseError(Error): pass def connect(): pass
调用异常的示范:
try: connect() except SocketTimeError as err: log.error(error)
except SocketRefuseError as err: log.error(error) except Error as err: log.error("API Unexpected error:%s" % err) except Exception as err: log.error("API bug cause exception.")
-
异常代码分离
异常处理与正常流程处理最好分离。
def action_executor(): action_a() action_b() action_c()
# 主函数力求简洁明了 def action(): try: action_executor() except ActionException as e: log.error(e) action() action_d()
二、REST API 编写技巧
Ref: http://www.pythondoc.com/Flask-RESTful/quickstart.html
The better solution than using abort
to signal errors for invalid API usage is to implement your own exception type and install an error handler for it that produces the errors in the format the user is expecting.
以下是个 “使用abort的过时的例子”。
from flask import Flask from flask.ext.restful import reqparse, abort, Api, Resource app = Flask(__name__) api = Api(app) TODOS = { 'todo1': {'task': 'build an API'}, 'todo2': {'task': '?????'}, 'todo3': {'task': 'profit!'}, } def abort_if_todo_doesnt_exist(todo_id): if todo_id not in TODOS: abort(404, message="Todo {} doesn't exist".format(todo_id)) parser = reqparse.RequestParser() parser.add_argument('task', type=str) # Todo # show a single todo item and lets you delete them class Todo(Resource): def get(self, todo_id): abort_if_todo_doesnt_exist(todo_id) return TODOS[todo_id] def delete(self, todo_id): abort_if_todo_doesnt_exist(todo_id) del TODOS[todo_id] return '', 204 def put(self, todo_id): args = parser.parse_args() task = {'task': args['task']} TODOS[todo_id] = task return task, 201 # TodoList # shows a list of all todos, and lets you POST to add new tasks class TodoList(Resource): def get(self): return TODOS def post(self): args = parser.parse_args() todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1 todo_id = 'todo%i' % todo_id TODOS[todo_id] = {'task': args['task']} return TODOS[todo_id], 201 ## ## Actually setup the Api resource routing here ## api.add_resource(TodoList, '/todos') api.add_resource(Todo, '/todos/<todo_id>') if __name__ == '__main__': app.run(debug=True)
正规系统学习
【慕课网-Python Flask构建可扩展的RESTful API-笔记】
标准的REST比较适合开放性的API。只负责提供数据,不负责业务逻辑。
标准的REST会造成HTTP请求的数量大幅度的增加。
HTTP状态码通常分为5种类型,分别以1~5五个数字开头,由3位整数组成: ------------------------------------------------------------------------------------------------ 200:请求成功 处理方式:获得响应的内容,进行处理 201:请求完成,结果是创建了新资源。新创建资源的URI可在响应的实体中得到 处理方式:爬虫中不会遇到 202:请求被接受,但处理尚未完成 处理方式:阻塞等待 204:服务器端已经实现了请求,但是没有返回新的信 息。如果客户是用户代理,则无须为此更新自身的文档视图。 处理方式:丢弃 300:该状态码不被HTTP/1.0的应用程序直接使用, 只是作为3XX类型回应的默认解释。存在多个可用的被请求资源。 处理方式:若程序中能够处理,则进行进一步处理,如果程序中不能处理,则丢弃 301:请求到的资源都会分配一个永久的URL,这样就可以在将来通过该URL来访问此资源 处理方式:重定向到分配的URL 302:请求到的资源在一个不同的URL处临时保存 处理方式:重定向到临时的URL 304 请求的资源未更新 处理方式:丢弃 400 非法请求 处理方式:丢弃 401 未授权 处理方式:丢弃 403 禁止 处理方式:丢弃 404 没有找到 处理方式:丢弃 5XX 回应代码以“5”开头的状态码表示服务器端发现自己出现错误,不能继续执行请求 处理方式:丢弃
一、自定义 APIException
Ref: flask 重写HTTPException类并自定义异常信息
Ref: 3.3 重构代码-自定义验证对象
Ref: HTTP Exception by werkzeug
This module implements a number of Python exceptions you can raise from within your views to trigger a standard non-200 response.
Flask:abort()函数 --> 直接返回“规范错误格式”,但有点不友好。例如:
@app.route('/abort0') def abort0(): abort(300)
二、继续
剩下的,主要是对 flask_restful的学习和实践。在此省略。
三、实战
-
自定义 部分
from werkzeug.exceptions import HTTPException from flask import json class ApiException(HTTPException): code = 500 status = "fail" information = "Unknown" def __init__(self, code=None, status=None, information=None, payload=None): Exception.__init__(self) if code: self.code = code if status: self.status = status if information: self.information = information self.payload = payload super(ApiException, self).__init__(information, None) def get_body(self, environ=None): body = dict(self.payload or ()) body['status'] = self.status body['information'] = self.information return body def get_headers(self, environ=None): return [('Content-Type', 'application/json')] # success class Success(ApiException): code = 200 status = "success" information = "Done" class WrongPassword(ApiException): code = 403 status = "fail" information = "todo" class WrongCheckCode(ApiException): code = 403 status = "fail" information = "todo" class SameUser(ApiException): code = 403 status = "fail" information = "todo" class WrongUserName(ApiException): code = 403 status = "fail" information = "todo" class WrongAuth(ApiException): code = 403 status = "fail" information = "User or project name is invalid." class ParameterException(ApiException): code = 403 status = "fail" information = "todo" class NoQuestionnaire(ApiException): code = 404 status = "fail" information = "todo" class NoProblem(ApiException): code = 404 status = "fail" information = "todo" class WrongProblemSecretKey(ApiException): code = 404 status = "fail" information = "todo" class SameIp(ApiException): code = 403 status = "fail" information = "todo" class WrongType(ApiException): code = 403 status = "fail" information = "todo" class WrongCode(ApiException): code = 403 status = "fail" information = "todo" class RequestError(ApiException): code = 400 status = "fail" information = "Please check request format or content." class InvalidPathOnServer(ApiException): code = 403 status = "fail" information = "Cannot find the path on Server"
-
raise部分
# 默认的
raise WrongAuth()
# 具体情况具体返回信息 raise RequestError(information='Please check Job mode again')
-
handler部分
def post(self): response_object = {'status': 'success'} response_object['information'] = 'Add task successfully.' train_tasks = [] try: train_tasks = self.run_one_task() except RequestError as err: print("<RequestError>: {}".format(err)) response_object['train_tasks'] = train_tasks response_object.update(err.get_body()) except InvalidPathOnServer as err: print("<InvalidPathOnServer>: {}".format(err)) response_object['train_tasks'] = train_tasks response_object.update(err.get_body()) except WrongAuth as err: print("<WrongAuth>: {}".format(err)) response_object['train_tasks'] = train_tasks response_object.update(err.get_body()) except ClientError as err: print("<ClientError>: {}".format(err)) response_object = {'status': 'fail'} response_object['information'] = 'Fail to connect with server resources.' response_object['train_tasks'] = train_tasks except Exception as err: print("<Exception>: {}".format(err)) response_object = {'status': 'fail'} response_object['information'] = 'Unknown errors.' response_object['train_tasks'] = train_tasks else: response_object['train_tasks'] = train_tasks return jsonify(response_object)
End.