Архив метки: RESTful

Let's have a REST, часть III

В части I рассказано о том, что такое REST и что значит для приложения быть RESTful. На несложном примере проиллюстрирован процесс проектирования RESTful приложения. В части II рассмотрены некоторые детали протокола HTTP в связи с реализацией на его основе RESTful приложений. В частности, рассказано, в чем разница между HTTP-методами POST и PUT, что такое идемпотентность, как обойти ограничения языка HTML и сделать браузерное HTML-приложение RESTful (ну, почти RESTful).

В данной, заключительной части, будут рассмотрены два RESTful приложения, написанные на Python с использованием микрофреймворка Flask. Оба приложения позволяют вести список книг, то есть, просматривать, добавлять, изменять и удалять книги из списка. Эти два приложения:

  • RESTful web-сервис и его клиент,
  • RESTful HTML-приложение, с которым пользователь работает в браузере.

Я не буду рассказывать о том, как установить Flask (соответствующая инструкция есть на сайте), а также об основах работы с Flask (об этом отлично рассказано в разделе Quickstart руководства пользователя).

Перейду сразу к делу и представлю код web-сервиса, возвращающего CSV-представление списка книг:

# -*- coding: utf-8 -*-

from flask import Flask, url_for

app = Flask(__name__)

books = {1 : [u'Лев Толстой', u'Война и мир']}
HEADERS = {'Content-Type' : 'text/csv; charset=utf-8'}

def csvbook(id):
return u"%s;%s;%sn" % (id, books[id][0], books[id][1])

@app.route('/')
@app.route('/books')
def index():
text = ''
for key in books.keys():
text += csvbook(key)
return text, 200, HEADERS


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

Поскольку приложение призвано продемонстрировать принципы REST, то все, что сопутствует этой демонстрации, написано как можно проще, чтобы занимать меньше места и быть понятным без объяснений. Так, книги будем хранить не в базе данных, а в словаре books, где ключ — целое число, а значение — список из двух строковых значений: автор книги, название книги.

Функция index() обрабатывает запросы GET для URL /books и /. Формируется CSV-представление списка книг из словаря books, используя функцию csvbook(id) для получения CSV-строки с данными каждой книги. Сформированное представление возвращается клиенту, причем ответ имеет статус 200 (OK) и HTTP-заголовок, задающий тип и кодировку возвращаемых данных.

Запустив наш сервис

C:> python restful-ws-01.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader

и введя в брузере адрес http://localhost:5000/books, получим файл, содержащий

1;Лев Толстой;Война и мир

Не очень удобно тестировать RESTful web-сервис с помощью браузера. В интернет-магазине Chrome есть приложение Advanced Rest Client, которое существенно упрощает ручное тестирование RESTful web-сервиса. Рекомендую попробовать.

Но в этой статье пойду другим путем и напишу клиента для нашего web-сервиса на Python:

# -*- coding: utf-8 -*-

import requests

def print_response(resp):
print " url: %s" % resp.url
print " status: %s %s" % (resp.status_code, resp.reason)
print "headers: %s " % resp.headers
print " data:n%s" % resp.text

print_response(requests.get("http://localhost:5000/books"))

Я использую библиотеку Requests, которая делает отправку HTTP-запросов с различными методами тривиальной задачей. Результат выполнения приведенного кода:

    url: http://localhost:5000/books
status: 200 OK
headers: CaseInsensitiveDict({'date': 'Thu, 13 Mar 2014 04:39:50 GMT', 'content-length': '45', 'content-type': 'text/csv; charset=utf-8', 'server': 'Werkzeug/0.9.4 Python/2.7.3'})
data:
1;Лев Толстой;Война и мир

Прежде чем реализовать следующие методы web-сервиса и написать для них клиентские запросы, приведу полный список ресурсов и методов web-сервиса:

/books        GET       получить список книг
/books POST создать новую книгу
/books/ GET получить данные книги
/books/ PUT изменить данные книги
/books/ DELETE удалить книгу

Вот код, реализующий перечисленные методы web-сервиса, а также обработчик для случая, когда запрошенный ресурс отсутствует:

# -*- coding: utf-8 -*-

from flask import Flask, request, abort, url_for

app = Flask(__name__)

books = {1 : [u'Лев Толстой', u'Война и мир']}
HEADERS = {'Content-Type' : 'text/csv; charset=utf-8'}

def csvbook(id):
return u"%s;%s;%s;%sn" %
(id, books[id][0], books[id][1], url_for('show', id=id, _external = True))

@app.route('/')
@app.route(&# 39;/books')
def index():
text = ''
for key in books.keys():
text += csvbook(key)
return text, 200, HEADERS

@app.route('/books', methods=['POST'])
def create():
new_id = len(books) + 1
books[new_id] = [request.form['author'], request.form['title']]
return csvbook(new_id), 201, HEADERS

@app.route('/books/')
def show(id):
if books.get(id):
return csvbook(id), 200, HEADERS
else:
abort(404)

@app.route('/books/', methods=['PUT'])
def update(id):
if books.get(id):
books[id] = [request.form['author'], request.form['title']]
else:
abort(404)
return csvbook(id), 200, HEADERS

@app.route('/books/', methods=['DELETE'])
def delete(id):
if books.get(id):
del books[id]
return u'OK', 200, HEADERS

@app.errorhandler(404)
def not_found(error):
return u'404: not found', 404, HEADERS


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

Ниже код клиента, тестирующий все методы нашего web-сервиса:

# -*- coding: utf-8 -*-

import requests

def print_response(resp):
print " url: %s" % resp.url
print " status: %s %s" % (resp.status_code, resp.reason)
print "headers: %s " % resp.headers
print " data:n%s" % resp.text

def list_books():
print("n### GET http://localhost:5000/booksn")
print_response(requests.get("http://localhost:5000/books"))


list_books

print("n### POST http://localhost:5000/booksn")
payload = {'author' : u'Александр Пушкин', 'title' : u'Пиковая дама'}
resp = requests.post("http://localhost:5000/books", data=payload)
print_response(resp)

list_books

print("n### PUT http://localhost:5000/books/2n")
payload = {'author' : u'Лев Толстой', 'title' : u'Анна Каренина'}
resp = requests.put("http://localhost:5000/books/2", data=payload)
print_response(resp)

list_books

print("n### DELETE http://localhost:5000/books/2n")
print_response(requests.delete("http://localhost:5000/books/2"))

list_books

print("n### GET http://localhost:5000/books/1n")
print_response(requests.get("http://localhost:5000/books/1"))

print("n### GET http://localhost:5000/books/2n")
print_response(requests.get("http://localhost:5000/books/2"))

Запустив web-сервис, выполните код клиента, чтобы убедиться в его работоспособности. Изучите выведенную клиентом информацию. Обратите внимание на ответы, которые возвращает каждый из методов web-сервиса клиенту.

Теперь перейдем к браузерному RESTful приложению. Оно поддерживает следующие ресурсы и операции:


/books GET получить представление списка книг
/books/new GET получить форму для ввода данных новой книги
/books POST создать новую книгу
/books/ GET получить представление книги
/books//edit GET получить форму для изменения данных книги
/books/ POST изменить данные книги
_method='PUT'
/books//delete POST удалить книгу
_method='DELETE'

Здесь адреса ресурсов следуют соглашениям фреймворка Ruby on Rails — законодателя мод в области RESTful web-приложений. Так как язык HTML не поддерживает запросы к серверу с методами PUT и DELETE, то эти методы имитируются при помощи скрытых полей форм с именем _method.

Ниже приведен код браузерного приложения:

# -*- coding: utf-8 -*-

from flask import Flask, request, redirect, abort

# GET and HEAD are safe
# GET, HEAD, PUT and DELETE are idempotent
# (RFC 2616 http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9)


app = Flask(__name__)
books = {1 : [u'Лев Толстой', u'Война и мир']}

list_books_template = u"""



Список книг



Список книг




%s
idАвторНазвание







"""

show_book_template = u"""



Книга



Книга






id%s
Автор%s
Название%s


К списку книг


"""

edit_book_template = u"""



Книга - Изменить



Книга - Изменить








id%s
Автор
Название








К списку книг


"""

new_book_template = u"""



Книга - Добавить



Книга - Добавить






Автор
Название




К списку книг


"""

error404_template = u"""



Список книг


Такой страницы нет :(




"""

@app.route('/')
@app.route('/books')
def index():
text = ''
for key, val in books.items():
text += u'Edit%s%s%s' % (key, key, key, val[0], val[1])
return list_books_template % text

@app.route('/books/new')
def new():
return new_book_template

@app.route('/books', methods=['POST'])
def create():
new_id = len(books) + 1
books[new_id] = [request.form['author'], request.form['title']]
return show_book_template % (new_id, books[new_id][0], books[new_id][1])


@app.route('/books//edit')
def edit(id):
if books.get(id):
return edit_book_template % (id, id, books[id][0], books[id][1], id)
else:
abort(404)

@app.route('/books/', methods=['GET', 'POST']) # PUT and DELETE
def show(id):
if request.method == 'GET':

# /books/ GET

if books.get(id):
return show_book_template % (id, books[id][0], books[id][1])
else:
abort(404)
elif request.method == 'POST' and request.form['_method'] == 'PUT':

# /books/ PUT

if books.get(id):
books[id] = [request.form['author'], request.form['title']]
else:
abort(404)
return show_book_template % (id, books[id][0], books[id][1])
elif request.method == 'POST' and request.form['_method'] == 'DELETE':

# /books/ DELETE

if books.get(id):
del books[id]
return redirect('/books')


@app.errorhandler(404)
def not_found(error):
return error404_template, 404


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

Запустив приложение

C:> python restful-server.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader

и введя в браузере адрес http://localhost:5000/books, попробуйте просматривать, изменять, добавлять и удалять книги.

В отличие от web-сервиса, в браузерном RESTful приложении большую роль играют гиперссылки, содержащиеся в представлениях (HTML-страницах), возвращаемых сервером клиенту (браузеру). Гиперссылки раскрывают перед пользователем структуру приложения, предлагая ему на выбор возможные варианты действий.

Пока всё. Let's have a rest!

Автор: Andrei Trofimov
Дата публикации: 2014-03-19T21:05:00.000+11:00