0,0
рейтинг
25 декабря 2014 в 18:43

Разработка → Проектирование RESTful API с помощью Python и Flask перевод tutorial

API*, Python*
В последние годы REST (REpresentational State Transfer) стала стандартной архитектурой при дизайне веб-сервисов и веб-API.

В этой статье я покажу вам как просто создавать RESTful веб-сервисы используя Python и микрофреймворк Flask.

Что такое REST?


Характеристика системы REST определяется шестью правилами дизайна:

  • Клиент-Сервер: Должно быть разделение между сервером, который предлагает сервис и клиентом, который использует ее.
  • Stateless: Каждый запрос от клиента должен содержать всю информацию, необходимую серверу для выполнения запроса. Другими словами, сервер не обязан сохранять информацию о состоянии клиента.
  • Кэширование: В каждом запросе клиента должно явно содержаться указание о возможности кэширования ответа и получения ответа из существующего кэша.
  • Уровневая система: Клиент может взаимодействовать не напрямую с сервером, а с произвольным количеством промежуточных узлов. При этом клиент может не знать о существовании промежуточных узлов, за исключением случаев передачи конфиденциальной информации.
  • Унификация: Унифицированный программный интерфейс сервера.
  • Код по запросу: Сервера могут поставлять исполняемый код или скрипты для выполнения их на стороне клиентов.


Что такое RESTful веб-сервис?


Архитектура REST разработана чтобы соответствовать протоколу HTTP используемому в сети Интернет.
Центральное место в концепции RESTful веб-сервисов это понятие ресурсов. Ресурсы представлены URI. Клиенты отправляют запросы к этим URI используя методы представленные протоколом HTTP, и, возможно, изменяют состояние этих ресурсов.
Методы HTTP спроектированы для воздействия на ресурс стандартным способом:
Метод HTTP Действие Пример
GET Получить информацию о ресурсе example.com/api/orders
(получить список заказов)
GET Получить информацию о ресурсе example.com/api/orders/123
(получить заказ #123)
POST Создать новый ресурс example.com/api/orders
(создать новый заказ из данных переданных с запросом)
PUT Обновить ресурс example.com/api/orders/123
(обновить заказ #123 данными переданными с запросом)
DELETE Удалить ресурс example.com/api/orders/123
(удалить заказ #123)


Дизайн REST не дает рекомендаций каким конкретно должен быть формат данных передаваемых с запросами. Данные переданные в теле запроса могут быть JSON blob, или с помощью аргументов в URL.

Проектируем простой веб-сервис


При проектировании веб-сервиса или API нужно определить ресурсы, которые будут доступны и запросы, с помощью которых эти данные будут доступны, согласно правил REST.
Допустим мы хотим написать приложение To Do List и мы должны спроектировать веб-сервис для него. Первое что мы должны сделать, это придумать кореневой URL для доступа к этому сервису. Например мы могли бы придумать в качестве корневого URL что-то типа:
http://[hostname]/todo/api/v1.0/

Здесь я решил включить в URL имя приложения и версию API. Добавление имени приложения в URL это хороший способ разделить между собой сервисы запущенные на одном сервере. Добавление версии API в URL может помочь, если вы захотите сделать обновление в будущем и внедрить в новой версии несовместимые функции и не хотите ломать работающие приложения которые работают на старом API.
Следующим шагом мы должны выбрать ресурсы, которые будут доступны через наш сервис. У нас очень простое приложение, у нас есть только задачи, поэтому нашими ресурсами могут быть только задачи из нашего ToDo листа.
Для доступа к ресурсам будем использовать следующие методы HTTP:
Метод HTTP URI Действие
GET http://[hostname]/todo/api/v1.0/tasks Получить список задач
GET http://[hostname]/todo/api/v1.0/tasks/[task_id] Получить задачу
POST http://[hostname]/todo/api/v1.0/tasks Создать новую задачу
PUT http://[hostname]/todo/api/v1.0/tasks/[task_id] Обновить существующую задачу
DELETE http://[hostname]/todo/api/v1.0/tasks/[task_id] Удалить задачу


Наша задача будет иметь следующие поля:
  • id: уникальный идентификатор задачи. Тип Numeric.
  • title: Краткое описание задачи. Тип String.
  • description: подробное описание задачи. Тип Text.
  • done: отметка о выполнении. Тип Boolean.


На этом мы заканчиваем часть посвященную дизайну нашего сервиса. Осталось только реализовать это!

Краткое введение в микрофреймворк Flask


Если вы читали серию Мега-Учебник Flask, вы знаете что Flask это простой и достаточно мощный веб-фреймворк на Python.
Прежде чем мы углубимся в специфику веб-сервисов, давайте рассмотрим как обычно реализованы приложения Flask.
Я предполагаю, что вы знакомы с основами работы с Python на вашей платформе. В примерах я буду использовать Unix-подобную операционную систему. Короче говоря, это озночает, что они будут работать на Linux, MacOS X и даже на Windows, если вы будете использовать Cygwin. Команды будут несколько отличаться, если вы будете использовать нативную версию Python для Windows.

Для начала установим Flask в виртуальном окружении. Если в вашей системе не установлен virtualenv, вы можете загрузить его из https://pypi.python.org/pypi/virtualenv.

$ mkdir todo-api
$ cd todo-api
$ virtualenv flask
New python executable in flask/bin/python
Installing setuptools............................done.
Installing pip...................done.
$ flask/bin/pip install flask


Теперь, когда Flask установлен давайте создадим простое веб приложение, для этого поместим следующий код в app.py:
#!flask/bin/python
from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return "Hello, World!"

if __name__ == '__main__':
    app.run(debug=True)


Чтобы запустить приложение, мы должны запустить app.py:

$ chmod a+x app.py
$ ./app.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader


Теперь вы можете запустить веб-браузер из набрать localhost:5000 чтобы увидеть наше маленькое приложение в действии.
Просто, не так ли? Теперь мы будем конвертировать наше приложение в RESTful сервис!

Реализация RESTful сервиса на Python и Flask


Создание веб-сервиса на Flask удивительно просто, гораздо проще, чем строить полноценные серверные приложения, вроде того, которое мы делали в серии Мега-Туториал.
Есть пара хороших расширений для Flask, которые могут облегчить создание RESTful сервисов, но наша задача настолько просто, что использование расширений будет излишним.
Клиенты нашего веб-сервиса будут просить сервис добавлять, удалять и модифицировать задачи, поэтому нам нужен простой способ хранить задачи. Очевидный способ сделать это — сделать небольшую базу данных, но, поскольку база данных выходи за рамки темы статьи, мы сделаем всё гораздо проще. Чтобы больше узнать о правильном использовании БД с Flask я снова рекомендую почитать мой Мега-Туториал.

Вместо базы данных мы будем хранить список наших задач в памяти. Это сработает, только если мы будем работать с сервером в один поток и в один процесс. Хоть для development-сервера это нормально, то для production-сервера это будет очень плохой идеей и будет лучше подумать об использовании базы данных.
Сейчас мы готовы реализовать первую точку входа в наш веб-сервис:
#!flask/bin/python
from flask import Flask, jsonify

app = Flask(__name__)

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
    }
]

@app.route('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
    return jsonify({'tasks': tasks})

if __name__ == '__main__':
    app.run(debug=True)


Как вы можете видеть, изменилось немногое. Мы создали в памяти задачи, которые являются не более чем простым массивом словарей. Каждая запись в массиве имеет все поля, которые мы определили выше для наших задач.
Вместо того, чтобы использовать точку входа index, у нас теперь есть функция get_tasks связанная с URI /todo/api/v1.0/tasks, для HTTP метода GET.
Вместо текста наша функция отдает JSON, в который Flask с помощью метода jsonify кодирует нашу структуру данных.
Использование веб-браузера, для тестирования веб-сервиса, не самая лучшая идея, т.к. с помощью веб-браузера не так просто генерировать все типы HTTP-запросов. Вместо этого мы будем использовать curl. Если curl у вас не установлен, лучше сделать это прямо сейчас.
Запустите веб-сервис тем же самым путем, как и демонстрационное приложение, запустив app.py. Теперь откройте новое окно консоли и вводите следующие команды:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 294
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 04:53:53 GMT

{
  "tasks": [
    {
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "done": false,
      "id": 1,
      "title": "Buy groceries"
    },
    {
      "description": "Need to find a good Python tutorial on the web",
      "done": false,
      "id": 2,
      "title": "Learn Python"
    }
  ]
}


Мы просто вызвали функцию нашего RESTful сервиса!

Сейчас давайте напишем вторую версию метода GET для наших задач. Если вы взгляните на таблицу выше, то следующим будет метод, который возвращает данные из одной задачи:
from flask import abort

@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
    task = filter(lambda t: t['id'] == task_id, tasks)
    if len(task) == 0:
        abort(404)
    return jsonify({'task': task[0]})


Вторая функция немного интересней. Здесь мы передаем через URL id задачи, и с помощью Flask транслируем в аргумент функции task_id.
С этим аргументом мы ищем нашу задачу в базе. Если полученный id не найдется в базе, мы вернем ошибку 404, которая по спецификации HTTP означает «Resource Not Found».
Если задача будет найдена, мы просто упакуем ее в JSON с помощью функции jsonify и отправим как ответ, так же как поступали раньше, отправляя коллекцию.
Вот так выглядит действие этой функции, когда мы вызываем ее с помощью curl:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 151
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:21:50 GMT

{
  "task": {
    "description": "Need to find a good Python tutorial on the web",
    "done": false,
    "id": 2,
    "title": "Learn Python"
  }
}
$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 238
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:21:52 GMT

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.</p><p>If you     entered the URL manually please check your spelling and try again.</p>


Когда мы запросили ресурс с id #2 мы получили его, но вместо ресурса с id #3 мы получили ошибку 404. Такую странную ошибку внутри HTML вместо JSON мы получили, потому, что Flask по умолчанию генерирует страницу с ошибкой 404. Так как это клиентские приложения будут всегда ожидать он нашего сервера JSON, то нам нужно изменить это поведение:
from flask import make_response

@app.errorhandler(404)
def not_found(error):
    return make_response(jsonify({'error': 'Not found'}), 404)


Так мы получим более соответствующий нашему API ответ:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks/3
HTTP/1.0 404 NOT FOUND
Content-Type: application/json
Content-Length: 26
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:36:54 GMT

{
  "error": "Not found"
}


Следующий в нашем списке метод POST, который мы будем использовать чтобы добавить новую задачу в нашу базу:
from flask import request

@app.route('/todo/api/v1.0/tasks', methods=['POST'])
def create_task():
    if not request.json or not 'title' 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': task}), 201


Добавление новой задачи тоже реализуется довольно просто. request.json содержит данные запроса, но только если они помечены как JSON. Если данных там нет, или данные на месте но отсутствует значение поля title, тогда возвращается код 400, который используется чтобы обозначить «Bad Request».
Затем мы создаем словарь с новой задачей, используя id последней задачи плюс 1(простой способ гарантировать уникальность id в нашей простой базе). Мы терпим отсутствие значения в поле description, и мы предполагаем что поле done при создании задачи всегда будет False.

Мы добавляем новую задачу к нашему массиву tasks, затем возвращаем клиенту сохраненную задачу и код 201, который в HTTP означает «Created».
Чтобы протестировать новую функцию мы используем следующую команду curl:

$ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"Read a book"}' http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 201 Created
Content-Type: application/json
Content-Length: 104
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:56:21 GMT

{
  "task": {
    "description": "",
    "done": false,
    "id": 3,
    "title": "Read a book"
  }
}


Примечание: если у вас Windows и вы используете Cygwin версию curl из bash тогда вышеописанная команда сработает как надо. Если вы используете нативную версию curl из обычно командной строки, то придется немного подшаманить с двойными кавычками:

curl -i -H "Content-Type: application/json" -X POST -d "{"""title""":"""Read a book"""}" http://localhost:5000/todo/api/v1.0/tasks


В Windows вы используете двойные кавычки чтобы отделить тело запроса, и внутри запроса двойные кавычки чтобы экранировать третю кавычку.
Конечно, после выполнения этого запроса мы можем получим обновленный список задач:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 423
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 05:57:44 GMT

{
  "tasks": [
    {
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "done": false,
      "id": 1,
      "title": "Buy groceries"
    },
    {
      "description": "Need to find a good Python tutorial on the web",
      "done": false,
      "id": 2,
      "title": "Learn Python"
    },
    {
      "description": "",
      "done": false,
      "id": 3,
      "title": "Read a book"
    }
  ]
}


Оставшиеся две функции нашего веб-сервиса будут выглядеть так:
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])
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': task[0]})

@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
    task = filter(lambda t: t['id'] == task_id, tasks)
    if len(task) == 0:
        abort(404)
    tasks.remove(task[0])
    return jsonify({'result': True})


Функция delete_task без сюрпризов. Для функции update_task мы стараемся предотвратить ошибки делая тщательную проверку входных аргументов. Мы должны убедиться, что предоставленные клиентом данные в надлежащем формате, прежде чем запишем их в базу.

Вызов функци обновляющей задачу с id #2 будет выглядеть примерно так:

$ curl -i -H "Content-Type: application/json" -X PUT -d '{"done":true}' http://localhost:5000/todo/api/v1.0/tasks/2
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 170
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 07:10:16 GMT

{
  "task": [
    {
      "description": "Need to find a good Python tutorial on the web",
      "done": true,
      "id": 2,
      "title": "Learn Python"
    }
  ]
}


Улучшаем интерфейс нашего сервиса


Сейчас основная проблема дизайна нашего сервиса в том, что клиенты вынуждены строить URI самостоятельно исходя из ID задач. Этот легко, но дает знание клиенту как строятся URI для доступа к данным, что может помешать в будущем, если мы захотим внести изменения в URI.
Вместо id задачи мы вернем полный URI, через который будет осуществляться выполнение всех действий с задачей. Для этого мы напишем маленькую функцию-хелпер, которая будет генерировать «публичную» версию задачи, отправляемую клиенту:
from flask import url_for

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


Все что мы делаем здесь это берем задачу из нашей базы данных и создаем новую задачу в которой все поля идентичны, за исключением поля id, которое заменено полем uri, сгенерированным функцией url_for предоставляемой Flask.
Когда мы возвращаем список задач мы прогоняем все задачи через эту функцию, прежде чем отослать клиенту:
@app.route('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
    return jsonify({'tasks': map(make_public_task, tasks)})


Теперь клиент получает вот такой список задач:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 406
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 18:16:28 GMT

{
  "tasks": [
    {
      "title": "Buy groceries",
      "done": false,
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
    },
    {
      "title": "Learn Python",
      "done": false,
      "description": "Need to find a good Python tutorial on the web",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
    }
  ]
}


Применив эту технику к остальным функциям мы сможем гарантировать, что клиент всегда получит URI, вместо id.

Защита RESTful веб-сервиса


Вы думали мы уже закончили? Конечно, мы закончили с функциональностью нашего сервиса, но у нас есть проблема. Наш сервис открыт для всех, а это не очень хорошо.
У нас есть законченый веб-сервис, который управляет нашим списком дел, но сервис, в текущем его состоянии, доступен каждому. Если незнакомец выяснит как работает наше API он или она может написать новый клиент и навести беспорядок в наших данных.
Многие руководства для начинающих игнорируют безопасность и заканчиваются здесь. По-моему это серьезная проблема, которая всегда должна быть решена.
Простой путь защитить наш веб-сервис это пускать клиентов после авторизации по логину и паролю. В обычном веб-приложении вы должны сделать форму логина, которая отправляет данные авторизации, сервер обрабатывает их и делает новую сессию, а браузер пользователя получает куки с идентификатором сессии. К сожаление здесь мы такое сделать не можем, stateless — одно из правил построения REST веб-сервисов и мы должны просить клиентов отправлять свои регистрационные данные при каждом запросе.
С REST мы всегда стараемся придерживаться протокола HTTP настолько, насколько сможем. Сейчас нам нужно реализовать аутентификацию пользователя в контексте HTTP, который предоставляет нам 2 варианта — Basic и Digest.

Существует маленькое расширение Flask написанное вашим покорным слугой. Давайте установим Flask-HTTPAuth:

$ flask/bin/pip install flask-httpauth


Теперь скажем нашего веб-сервису отдавать данные только пользователю с логином miguel и паролем python. Для начала настроим Basic HTTP authentication как показано ниже:
from flask.ext.httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()

@auth.get_password
def get_password(username):
    if username == 'miguel':
        return 'python'
    return None

@auth.error_handler
def unauthorized():
    return make_response(jsonify({'error': 'Unauthorized access'}), 401)


Функция get_password будет по имени пользователя возвращать пароль. В более сложных системах такая функцию должна будет лезть в базу, но для одного пользователя это не обязательно.
Функция error_handler будет использоваться чтобы отправить ошибку авторизации, при неправильных данных. Так же как мы поступили с другими ошибками мы должны настроить функцию на отправку JSON, вместо HTML.

После настройки системы аутентификаци, осталось только добавить декоратор@auth.login_required для всех функций, которые должны быть защищены. Например:
@app.route('/todo/api/v1.0/tasks', methods=['GET'])
@auth.login_required
def get_tasks():
    return jsonify({'tasks': tasks})


Если мы попробуем запросить эту функцию с помощью curl мы получим примерно следующее:

$ curl -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 401 UNAUTHORIZED
Content-Type: application/json
Content-Length: 36
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 06:41:14 GMT

{
  "error": "Unauthorized access"
}


Для того, чтобы вызвать эту функцию, мы должны подтвердить наши полномочия:

$ curl -u miguel:python -i http://localhost:5000/todo/api/v1.0/tasks
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 316
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Mon, 20 May 2013 06:46:45 GMT

{
  "tasks": [
    {
      "title": "Buy groceries",
      "done": false,
      "description": "Milk, Cheese, Pizza, Fruit, Tylenol",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/1"
    },
    {
      "title": "Learn Python",
      "done": false,
      "description": "Need to find a good Python tutorial on the web",
      "uri": "http://localhost:5000/todo/api/v1.0/tasks/2"
    }
  ]
}


Расширение с аутентификацией дает нам свободу выбирать какие функции будут в общем доступе, а какие защищены.
Для защиты регистрационной информации наш веб-сервис должен быть доступен через HTTP Secure server ( ...) который шифрует траффик между клиентом и сервером и предотвращает получение конфиденциальной информаци третьей стороной.
К сожалению веб-браузеры имеют дурную привычку показывать страшное диалоговое окно, когда запрос возвращается с ошибкой 401. Это происходит даже для фоновых запросов, так что если бы мы реализовали клиента для веб-браузера, нам пришлось бы прыгать через обручи, чтобы не давать браузеру показывать свои окна.
Простой путь обмануть браузер — возвращать любой другой код, вместо 401. Любимая всеми альтернатива это код 403, который означает ошибку «Forbidden». Хоть это достаточно близкая по смыслу ошибка, это нарушает стандарт HTTP, так что это неправильно. В частности будет хорошим решением не использовать веб-браузер в качестве клиентского приложения. Но в случаях, когда сервер и клиент разрабатываются совместно это спасает от многих неприятностей. Чтобы провернуть этот трюк нам нужно просто заменить код ошибки с 401 на 403:
@auth.error_handler
def unauthorized():
    return make_response(jsonify({'error': 'Unauthorized access'}), 403)


В клиентском приложении нужно тоже отлавливать ошибку 403.

Возможные улучшения


Есть несколько возможностей улучшить разработанный нами сегодня веб-сервис.
Для начала, настоящий веб-сервис должен общаться с настоящей базой данных. Структура данных в памяти очень ограниченный способ хранения данных и он не должен использоваться в реальных приложениях.
Другой способ улучшить приложение это поддержка нескольких пользователей. Если система поддерживает несколько пользователей, то данные аутентификации могут использоваться чтобы возвращать персональные списки пользователям. В такой системе пользователи станут вторым ресурсом. Запрос POST будет регистрировать нового пользователя в системе. Запрос GET может возвращать информацию о пользователе. Запрос PUT может обновлять информацию о пользователе, например email. Запрос DELETE будет удалять пользователя из системы.
Запрос GET, который возвращает список задач, может быть расширен несколькими способами. Для начала это запрос может иметь опциональные агрументы, такие как количество задач на страницу. Другой путь сделать функцию более удобной это добавить критерии фильтрации. Например клиент может запросить только выполненые задачии или задачи, заголовок которых начинается с определенной буквы. Все эти элементы могут быть добавлены в URL как аргументы.

Вывод


Законченый код для веб-сервиса To Do List вы можете взять здесь: https://gist.github.com/miguelgrinberg/5614326.
Я верю что это было простое и дружелюбное введение в RESTful API. Если есть достаточный инетерес я мог бы написать вторую часть этой статьи, в которой мы разработаем простой веб-клиент для нашего сервиса.
Я сделал клиента для нашего сервиса:Writing a Javascript REST client.
Статья о таком же сервере, но с использованием Flask-RESTfulDesigning a RESTful API using Flask-RESTful.
Miguel
Перевод: Miguel Grinberg
Найденый Станислав @NCNecros
карма
19,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (31)

  • +2
    Мы ранее писали по теме: habrahabr.ru/company/SECL_GROUP/blog/204510/ — Как мы делали API для социальной сети (REST API для WEB)
  • +6
    Делать переводы про клиенты к этому сервису, да и остальных интересных статей по python и flask из этого блога? Я не профессиональный переводчик и не профессиональный python-разработчик — переводы эти делаю для практики языка и для новичков, которые как и я не могут найти в рунете такого же качества информации. Поэтому мой слог может хромать, перевод напоминать гугл-транслейт (хотя максимальная единица автоматического перевода — фраза)), а места расстановки запятых и тире может заставить дергаться глаз филолога. Если кто-то поможет с вычиткой переводов перед публикацией, буду очень признателен.
    • 0
      Про AngularJS в этой связке неплохо бы разжевать, если оно есть в природе.
      • 0
        Есть интересная статься про Django + rest_framework и AngularJS: https://thinkster.io/brewer/angular-django-tutorial/
        • 0
          Спасибо, но NCNecros про конкретные статьи Гринберга ведь спрашивал
      • 0
        Ну, вообще Angular ведь совместим на уровне спецификации RESTful, и тут вполне применимы стандартные мануалы по взаимодействию с REST. Единственное отличие — это то, как правильно настроить asset pipeline конкретного фреймворка (а с Flask всё должно быть относительно просто).
  • 0
    Всегда удивляло, почему для создания ресурса используют POST, а для обновления — PUT, семантически логичнее использовать PUT для создания, PATCH — для обновления…
    • 0
      Требования стандарта?
      • 0
        эмм… какого стандарта?
        • 0
          Ну может со словом «стандарт» я перегнул немного.
          Однако это общепринятое применение методов.
          Можно почитать здесь, здесь книжка в PDF, а вот и статья на хабре.
          Прям вот единого сборника требований (спецификации) к RESTful API я не нашел.
          Можете сами в гугле поискать. Но везде применение данных методов именно такое.
          • 0
            Это я понимаю, просто в RFC метод PATCH изначально задумывался для изменения содержимого ресурса :)
            Большое спасибо за ссылку на книжку!
            • 0
              PATCH — частичное изменение ресурса. PUT- полное.
          • 0
            Как жалко что нет кармы, чтобы заплюсовать! Отличнейший комментарий!!!
    • –1
      Если почитать всякие статьи из серии «Делаем фейсбук за неделю», так там вообще пишут, что методов 2 — GET и POST, а остальные есть но не нужны\не используются\что-то из теории.
      • +1
        Ну там несколько ненужны сколько ограничения браузеров. Еще не так давно вы не могли использовать ничего другого. Да и XMLHttpRequest только последние несколько лет научился остальные методы принимать. Формы до сих пор не умеют.
    • 0
      Дело в том, что на момент создания ресурса его URL зачастую еще не известен, поэтому запрос PUT посылать попросту некуда.

      Посылать запрос PUT на URL вида /create или /new нельзя — потому что запрос PUT обязан быть идемпотентным.
  • +2
    Пара замечаний по сути статьи:
    — после django-rest-framework данный код выглядит как закат солнца вручную, в django-reset-framework можно при помощи десятка строк реализовать полноценный REST API, с pagination, документацией, api browser и поддержкой всех методов
    — не рекомендуется в REST API передавать номер версии в url
    • 0
      Вот первое, увы, да. Насколько приятная и компактная штука flask, но аналогов DRF и Tastypie с их магической автоматизацией там совершенно нет.
      • –2
        Это отличное DDD проектирование с возможностью выделения сервисов для SOA, но ничего не мешает загрузить схему БД и написать RESTful сервис самому, что бы было понятнее, с такими маршрутами. Tastypie и DRF прекрасно справляется с этой задачей, но вот нормально выделить ААА сервисы бывает довольно сложно.
        • +1
          Вы извините, конечно, но по-моему, ваша фраза написана на машинном языке)
    • 0
      Я думаю, а точнее, мне недавно начало казаться, что сравнивать django & flask совсем не нужно. Абсолютно разные фреймоврки, которые хоть и реализуют одну и туже задачу, но делают это с абсолютно разным мировоззрением. :)
  • +1
    В официальной документации есть еще более приятный пример реализации REST API:
    flask.pocoo.org/docs/0.10/views/#method-views-for-apis
    • 0
      И правда интересная и полезная информация. Спасибо.
  • +1
    Окошко авторизации возникает совсем не потому, что код ответа 401, а из-за заголовка WWW-Authenticate в ответе. Вот здесь есть более подробный ответ. Если вам не нужно выводить это окно пользователю, просто разберитесь, откуда берется этот заголовок и не надо устраивать махинаций с кодами ответа, пожалуйста
  • +1
    Не стоит предлагать городить функциональщину на Python там, где ее не должно быть:

    def get_task(task_id):
        for task in tasks:
            if t['id'] == tasks_id:
                return jsonify({'task': task})
        abort(404)
    


    Функцию make_public_task можно написать проще:

    def make_public_task(task):
        return dict(k, v for k, v in task.items() if k != 'id', 
                    url=url_for('get_task', task_id=task['id'], _external=True))
    


    Вместо if len(task) == 0 лучше писать просто if not task.

    Команду return None в конце тела функции писать не нужно — это делается автоматически.

    Проверку типов необходимо делать с помощью функции isinstance(obj, cls), конкретно в случае со строками используя isinstance(s, basestring) (актуально для Python 2).
    • 0
      Всегда восхищался такими людьми как Вы, которые могут алгоритм завернуть в пару строк.
  • 0
    Не стоит забывать про метод PATCH. Он предназначен для частичного изменения объекта. На мой взгляд, в большинстве своём он гораздо нужнее, чем PUT.
  • 0
    Спасибо за статью. Не жалею, что прочитал.
  • 0
    Я не очень понимаю как с REST API в python избежать проблемы одновременных обращений.:
    Добавим time.sleep(1000), что бы просимулировать медленный ответ REST API и тут начинаются проблемы:
    @app.route('/tasks/<int:task_id>', methods=['GET'])
    def get_task(task_id):
        task = filter(lambda t: t['id'] == task_id, tasks)
        if len(task) == 0:
            abort(404)
        if task_id==2:
            print "sleeping..."
            time.sleep(1000)
        return jsonify({'task': task[0]})
    

    я паралельно запросил curl -i localhost:8888/tasks/2 и curl -i localhost:8888/tasks/1, при этом нет ответа от этих двух запросов.
    #python flask/example.py
     * Running on http://0.0.0.0:8888/
     * Restarting with reloader
    127.0.0.1 - - [21/Jan/2015 14:06:47] "GET /tasks/3 HTTP/1.1" 404 -
    127.0.0.1 - - [21/Jan/2015 14:06:51] "GET /tasks/1 HTTP/1.1" 200 -
    sleeping...
    


    Подскажите как решается эта проблема?
    • 0
      Либо куча воркеров и очереди сообщений/задач, либо неблокирующий ioloop и асинхронщина (aiohttp/aiorest, twisted/tornado).

      time.sleep() вообще блокирует поток, что и сказано в документации.

      Конкретно для фласка есть вот такая штука, но однажды начав писать асинхронный код, я стал смотреть на такие решения с недоумением и от него [фласка] давно убежал.

      Вообще WSGI синхронный на уровне архитектуры, т.е. один процесс/поток обрабатывает один запрос.
      • 0
        Убежали от Flask-а. А куда?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.