Baixe o app para aproveitar ainda mais
Prévia do material em texto
Implementação de um servidor rest Projetando uma API RESTful usando Flask-RESTful Este é o terceiro artigo em que eu explorar diferentes aspectos da escrita RESTful APIs usando o Microframework Flask. Aqui está o primeiro (https://blog.miguelgrinberg.com/post/designing-a-restful-api-with-python-and-flask) e o segundo(https://blog.miguelgrinberg.com/post/writing-a-javascript-rest-client). O servidor RESTful de exemplo que eu escrevi antes usava apenas Flask como uma dependência. Hoje vou mostrar-lhe como escrever o mesmo servidor usandoFlask-RESTful,(http://flask-restful.readthedocs.io/en/latest/ )uma extensão Flask que simplifica a criação de APIs. OBS: Flask-RESTful é uma extensão para o Flask que adiciona suporte para criar rapidamente APIs REST. É uma abstração leve que funciona com o seu ORM / bibliotecas existentes. O Flask-RESTful incentiva as melhores práticas com uma configuração mínima. Se você estiver familiarizado com Flask, Flask-RESTful deve ser fácil de pegar. O servidor RESTful Como lembrete, aqui está a definição do serviço da Web ToDo List que tem servido como um exemplo em artigos RESTful: HTTP Method URI Action GET http://[hostname]/todo/api/v1.0/tasks Recupera lista de tarefas GET http://[hostname]/todo/api/v1.0/tasks/[task_id] Recupera uma tarefa POST http://[hostname]/todo/api/v1.0/tasks Cria uma tarefa PUT http://[hostname]/todo/api/v1.0/tasks/[task_id] Atualizar uma tarefa existente DELETE http://[hostname]/todo/api/v1.0/tasks/[task_id] Excluir uma tarefa O único recurso exposto por este serviço é uma "tarefa", que tem os seguintes campos de dados: Uri: URI exclusivo para a tarefa. Tipo de cadeia. Title: descrição da tarefa curta. Tipo de cadeia. description: descrição da tarefa longa. Tipo de texto. Done: estado de conclusão da tarefa. Tipo booleano. Routing No meu primeiro exemplo de servidor RESTful (código-fonte aqui), usei funções regulares do Flask para definir todas as rotas. #!flask/bin/python import six from flask import Flask, jsonify, abort, request, make_response, url_for from flask.ext.httpauth import HTTPBasicAuth app = Flask(__name__, static_url_path="") auth = HTTPBasicAuth() @auth.get_password def get_password(username): if username == 'miguel': return 'python' return None @auth.error_handler def unauthorized(): # return 403 instead of 401 to prevent browsers from displaying the default # auth dialog return make_response(jsonify({'error': 'Unauthorized access'}), 403) @app.errorhandler(400) def bad_request(error): return make_response(jsonify({'error': 'Bad request'}), 400) @app.errorhandler(404) def not_found(error): return make_response(jsonify({'error': 'Not found'}), 404) tasks = [ { 'id': 1, 'title': u'Buy groceries', 'description': u'Milk, Cheese, Pizza, Fruit, Tylenol', 'done': False }, { 'id': 2, 'title': u'Learn Python', 'description': u'Need to find a good Python tutorial on the web', 'done': False } ] def make_public_task(task): new_task = {} for field in task: if field == 'id': new_task['uri'] = url_for('get_task', task_id=task['id'], _external=True) else: new_task[field] = task[field] return new_task @app.route('/todo/api/v1.0/tasks', methods=['GET']) @auth.login_required def get_tasks(): return jsonify({'tasks': [make_public_task(task) for task in tasks]}) @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET']) @auth.login_required def get_task(task_id): task = [task for task in tasks if task['id'] == task_id] if len(task) == 0: abort(404) return jsonify({'task': make_public_task(task[0])}) @app.route('/todo/api/v1.0/tasks', methods=['POST']) @auth.login_required def create_task(): if not request.json or 'title' not in request.json: abort(400) task = { 'id': tasks[-1]['id'] + 1, 'title': request.json['title'], 'description': request.json.get('description', ""), 'done': False } tasks.append(task) return jsonify({'task': make_public_task(task)}), 201 @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT']) @auth.login_required def update_task(task_id): task = [task for task in tasks if task['id'] == task_id] if len(task) == 0: abort(404) if not request.json: abort(400) if 'title' in request.json and \ not isinstance(request.json['title'], six.string_types): abort(400) if 'description' in request.json and \ not isinstance(request.json['description'], six.string_types): abort(400) if 'done' in request.json and type(request.json['done']) is not bool: abort(400) task[0]['title'] = request.json.get('title', task[0]['title']) task[0]['description'] = request.json.get('description', task[0]['description']) task[0]['done'] = request.json.get('done', task[0]['done']) return jsonify({'task': make_public_task(task[0])}) @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE']) @auth.login_required def delete_task(task_id): task = [task for task in tasks if task['id'] == task_id] if len(task) == 0: abort(404) tasks.remove(task[0]) return jsonify({'result': True}) if __name__ == '__main__': app.run(debug=True) Flask-RESTful fornece uma classe base Resource que pode definir o roteamento para um ou mais métodos HTTP para um determinado URL. Por exemplo, para definir um recurso de usuário com métodos GET, PUT e DELETE você escreveria: from flask import Flask from flask.ext.restful import Api, Resource app = Flask(__name__) api = Api(app) class UserAPI(Resource): def get(self, id): pass def put(self, id): pass def delete(self, id): pass api.add_resource(UserAPI, '/users/<int:id>', endpoint = 'user') A função add_resource registra as rotas com o framework usando o determinado nó de extremidade. Se um nó de extremidade não for dado, então o Flask-RESTful gera um para você a partir do nome da classe, mas como às vezes o nó de extremidade é necessário para funções como url_for eu prefiro torná-lo explícito. Minha API To Do define dois URLs: /todo/api/v1.0/tasks para a lista de tarefas e /todo/api/v1.0/tasks/ <int: id> para uma tarefa individual. Uma vez que a classe Flask-RESTful Resource pode envolver uma única URL, este servidor precisará de dois recursos: class TaskListAPI(Resource): def get(self): pass def post(self): pass class TaskAPI(Resource): def get(self, id): pass def put(self, id): pass def delete(self, id): pass api.add_resource(TaskListAPI, '/todo/api/v1.0/tasks', endpoint = 'tasks') api.add_resource(TaskAPI, '/todo/api/v1.0/tasks/<int:id>', endpoint = 'task') Observe que, enquanto as exibições de método da TaskList API não recebem nenhum argumento, os que estão no TaskAPIrecebem todos o id, conforme especificado na URL sob a qual o recurso está registrado. Solicitação de análise e validação Quando eu implementei este servidor no artigo anterior eu fiz a minha própria validação dos dados do pedido. Por exemplo, observe quanto tempo o manipulador PUT está nessa versão: @app.route('/todo/api/v1.0/tasks/<int:task_id>', methods = ['PUT']) @auth.login_required def update_task(task_id): task = filter(lambda t: t['id'] == task_id, tasks) if len(task) == 0: abort(404) if not request.json: abort(400) if 'title' in request.json and type(request.json['title']) != unicode: abort(400) if 'description' in request.json and type(request.json['description']) is not unicode: abort(400) if 'done' in request.json and type(request.json['done']) is not bool: abort(400) task[0]['title'] = request.json.get('title', task[0]['title']) task[0]['description'] = request.json.get('description', task[0]['description']) task[0]['done'] = request.json.get('done', task[0]['done']) return jsonify( { 'task': make_public_task(task[0]) } ) Aqui eu tenho que ter certeza que os dados fornecidos com o pedido é válido antes de usá-lo, e isso torna a função muito longa. Flask-RESTful fornece uma maneira muito melhor de lidar com isso com a classe RequestParser. Esta classe funciona de forma semelhante a argparse para argumentos de linha de comando. Primeiro, para cada recurso eu defino os argumentos e como validá-los: from flask.ext.restful import reqparse class TaskListAPI(Resource): def __init__(self): self.reqparse = reqparse.RequestParser() self.reqparse.add_argument('title', type = str, required = True, help = 'No task title provided', location = 'json') self.reqparse.add_argument('description', type = str, default = "", location = 'json') super(TaskListAPI, self).__init__() # ... class TaskAPI(Resource): def __init__(self): self.reqparse = reqparse.RequestParser() self.reqparse.add_argument('title', type = str, location = 'json') self.reqparse.add_argument('description', type = str, location = 'json') self.reqparse.add_argument('done', type = bool, location = 'json') super(TaskAPI, self).__init__() # ... No recurso TaskListAPI o método POST é o único que recebe os argumentos. O argumento de title é necessário aqui, então eu incluí uma mensagem de erro que Flask-RESTful enviará como uma resposta para o cliente quando o campo está faltando. O campo de description é opcional e, quando ele estiver faltando, um valor padrão de uma seqüência vazia será usado. Um aspecto interessante da classe RequestParser é que, por padrão, ele procura campos em request.values, então o argumento opcional location deve ser definido para indicar que os campos estão vindo em request.json. O analisador de solicitações para o TaskAPI é construído de forma semelhante, mas tem algumas diferenças. Neste caso é o método PUT que precisará analisar argumentos, e para este método todos os argumentos são opcionais, incluindo o campo done que não fazia parte da solicitação no outro recurso. Agora que os analisadores de solicitação são inicializados, analisar e validar um pedido é bastante fácil. Por exemplo, observe quanto mais simples o método TaskAPI.put () se torna: def put(self, id): task = filter(lambda t: t['id'] == id, tasks) if len(task) == 0: abort(404) task = task[0] args = self.reqparse.parse_args() for k, v in args.iteritems(): if v != None: task[k] = v return jsonify( { 'task': make_public_task(task) } ) Um benefício lateral de deixar Flask-RESTful fazer a validação é que agora não há necessidade de ter um manipulador para o código de pedido erro 400 errado, isso é tudo cuidado pela extensão. Gerando respostas Meu servidor REST original gera as respostas usando a função auxiliar jsonify do Flask. Flask-RESTful automaticamente manipula a conversão para JSON, então em vez disso: return jsonify( { 'task': make_public_task(task) } ) Eu posso fazer isso: return { 'task': make_public_task(task) } Flask-RESTful também suporta a devolução de um código de status personalizado quando necessário: return { 'task': make_public_task(task) }, 201 Mas há mais. O make_public_task wrapper do servidor original converteu uma tarefa de sua representação interna para a representação externa esperada pelos clientes. A conversão incluiu remover o campo id e adicionar um campo uri em seu lugar. Flask-RESTful fornece uma função auxiliar para fazer isso de uma forma muito mais elegante que gera não apenas o uri, mas também faz a conversão de tipo nos campos restantes: from flask.ext.restful import fields, marshal task_fields = { 'title': fields.String, 'description': fields.String, 'done': fields.Boolean, 'uri': fields.Url('task') } class TaskAPI(Resource): # ... def put(self, id): # ... return { 'task': marshal(task, task_fields) } A estrutura task_fields serve como um modelo para a função marshal. O tipo fields.Url é um tipo especial que gera um URL. O argumento que leva é o ponto final (lembre-se de que eu usei pontos de extremidade explícitos quando registrei os recursos especificamente para que eu possa me referir a eles quando necessário). Autenticação As rotas no servidor REST estão protegidas com autenticação básica HTTP. No servidor original, a proteção foi adicionada usando o decorador fornecido pela extensão Flask-HTTPAuth. Como a classe Resouce herda do MethodView do Flask, é possível anexar decoradores aos métodos definindo uma variável de classe decorators: from flask.ext.httpauth import HTTPBasicAuth # ... auth = HTTPBasicAuth() # ... class TaskAPI(Resource): decorators = [auth.login_required] # ... class TaskAPI(Resource): decorators = [auth.login_required] # ... Conclusão A implementação completa do servidor baseada em Flask-RESTful está disponível no meu projeto REST-tutorial no github (https://github.com/miguelgrinberg/REST-tutorial). aqui ficam todos os códigos: index.html https://github.com/miguelgrinberg/REST-tutorial/blob/master/static/index.html .gitignore https://github.com/miguelgrinberg/REST-tutorial/blob/master/.gitignore license https://github.com/miguelgrinberg/REST-tutorial/blob/master/LICENSE readme https://github.com/miguelgrinberg/REST-tutorial/blob/master/README.md Instale o Python 2.7 eo git. Execute setup.sh (Linux, OS X, Cygwin) ou setup.bat (Windows) Execute ./rest-server.py para iniciar o servidor (no Windows use flask \ Scripts \ python rest-server.py em vez disso) Como alternativa, execute ./rest-server-v2.py para iniciar a versão RESTful Flask do servidor. Abra http: // localhost: 5000 / index.html no seu navegador da Web para executar o cliente requirements.txt https://github.com/miguelgrinberg/REST-tutorial/blob/master/requirements.txt rest-server-v2.py https://github.com/miguelgrinberg/REST-tutorial/blob/master/rest-server-v2.py rest-server.py https://github.com/miguelgrinberg/REST-tutorial/blob/master/rest-server.py setup.bat https://github.com/miguelgrinberg/REST-tutorial/blob/master/setup.bat setup.sh https://github.com/miguelgrinberg/REST-tutorial/blob/master/setup.sh virtualenv.py https://github.com/miguelgrinberg/REST-tutorial/blob/master/virtualenv.py O arquivo com o servidor Flask-RESTful é rest-server-v2.py. (https://github.com/miguelgrinberg/REST-tutorial/blob/master/rest-server-v2.py) servidor rest-server-v2.py #!flask/bin/python """Alternative version of the ToDo RESTful server implemented using the Flask-RESTful extension.""" from flask import Flask, jsonify, abort, make_response from flask.ext.restful import Api, Resource, reqparse, fields, marshal from flask.ext.httpauthimport HTTPBasicAuth app = Flask(__name__, static_url_path="") api = Api(app) auth = HTTPBasicAuth() @auth.get_password def get_password(username): if username == 'miguel': return 'python' return None @auth.error_handler def unauthorized(): # return 403 instead of 401 to prevent browsers from displaying the default # auth dialog return make_response(jsonify({'message': 'Unauthorized access'}), 403) tasks = [ { 'id': 1, 'title': u'Buy groceries', 'description': u'Milk, Cheese, Pizza, Fruit, Tylenol', 'done': False }, { 'id': 2, 'title': u'Learn Python', 'description': u'Need to find a good Python tutorial on the web', 'done': False } ] task_fields = { 'title': fields.String, 'description': fields.String, 'done': fields.Boolean, 'uri': fields.Url('task') } class TaskListAPI(Resource): decorators = [auth.login_required] def __init__(self): self.reqparse = reqparse.RequestParser() self.reqparse.add_argument('title', type=str, required=True, help='No task title provided', location='json') self.reqparse.add_argument('description', type=str, default="", location='json') super(TaskListAPI, self).__init__() def get(self): return {'tasks': [marshal(task, task_fields) for task in tasks]} def post(self): args = self.reqparse.parse_args() task = { 'id': tasks[-1]['id'] + 1, 'title': args['title'], 'description': args['description'], 'done': False } tasks.append(task) return {'task': marshal(task, task_fields)}, 201 class TaskAPI(Resource): decorators = [auth.login_required] def __init__(self): self.reqparse = reqparse.RequestParser() self.reqparse.add_argument('title', type=str, location='json') self.reqparse.add_argument('description', type=str, location='json') self.reqparse.add_argument('done', type=bool, location='json') super(TaskAPI, self).__init__() def get(self, id): task = [task for task in tasks if task['id'] == id] if len(task) == 0: abort(404) return {'task': marshal(task[0], task_fields)} def put(self, id): task = [task for task in tasks if task['id'] == id] if len(task) == 0: abort(404) task = task[0] args = self.reqparse.parse_args() for k, v in args.items(): if v is not None: task[k] = v return {'task': marshal(task, task_fields)} def delete(self, id): task = [task for task in tasks if task['id'] == id] if len(task) == 0: abort(404) tasks.remove(task[0]) return {'result': True} api.add_resource(TaskListAPI, '/todo/api/v1.0/tasks', endpoint='tasks') api.add_resource(TaskAPI, '/todo/api/v1.0/tasks/<int:id>', endpoint='task') if __name__ == '__main__': app.run(debug=True) Você também pode baixar todo o projeto, incluindo implementações de servidor e um cliente javascript para testá-lo: na pasta em anexo.
Compartilhar