В части 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;%s\n" % (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/<id> GET получить данные книги <id>
/books/<id> PUT изменить данные книги <id>
/books/<id> DELETE удалить книгу <id>
Вот код, реализующий перечисленные методы 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;%s\n" % \
(id, books[id][0], books[id][1], url_for('show', id=id, _external = True))
@app.route('/')
@app.route('/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/<int:id>')
def show(id):
if books.get(id):
return csvbook(id), 200, HEADERS
else:
abort(404)
@app.route('/books/<int:id>', 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/<int:id>', 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/books\n")
print_response(requests.get("http://localhost:5000/books"))
list_books
print("\n### POST http://localhost:5000/books\n")
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/2\n")
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/2\n")
print_response(requests.delete("http://localhost:5000/books/2"))
list_books
print("\n### GET http://localhost:5000/books/1\n")
print_response(requests.get("http://localhost:5000/books/1"))
print("\n### GET http://localhost:5000/books/2\n")
print_response(requests.get("http://localhost:5000/books/2"))
Запустив web-сервис, выполните код клиента, чтобы убедиться в его работоспособности. Изучите выведенную клиентом информацию. Обратите внимание на ответы, которые возвращает каждый из методов web-сервиса клиенту.
Теперь перейдем к браузерному RESTful приложению. Оно поддерживает следующие ресурсы и операции:
/books GET получить представление списка книг
/books/new GET получить форму для ввода данных новой книги
/books POST создать новую книгу
/books/<id> GET получить представление книги <id>
/books/<id>/edit GET получить форму для изменения данных книги <id>
/books/<id> POST изменить данные книги <id>
_method='PUT'
/books/<id>/delete POST удалить книгу <id>
_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"""
<!DOCTYPE html>
<html>
<head>
<title>Список книг</title>
<style type="text/css">th {background-color:#cceecc;}</style>
</head>
<body>
<h2>Список книг</h2>
<table>
<tr><th></th><th>id</th><th>Автор</th><th>Название</th></tr>
%s
</table>
<p></p>
<form action="/books/new" method="GET">
<input type="submit" value="Добавить книгу" />
</form>
</body>
</html>
"""
show_book_template = u"""
<!DOCTYPE html>
<html>
<head>
<title>Книга</title>
<style type="text/css">.header {font-weight:bold;background-color:#cceecc;}</style>
</head>
<body>
<h2>Книга</h2>
<table>
<tr><td class="header">id</td><td>%s</td></tr>
<tr><td class="header">Автор</td><td>%s</td></tr>
<tr><td class="header">Название</td><td>%s</td></tr>
</table>
<p></p>
<a href="/books">К списку книг</a>
</body>
</html>
"""
edit_book_template = u"""
<!DOCTYPE html>
<html>
<head>
<title>Книга - Изменить</title>
<style type="text/css">.header {font-weight:bold;background-color:#cceecc;}</style>
</head>
<body>
<h2>Книга - Изменить</h2>
<form action="/books/%s" method="POST">
<input type="hidden" name="_method" value="PUT" />
<table>
<tr><td class="header">id</td><td>%s</td></tr>
<tr><td class="header">Автор</td><td><input type="text" name="author" value="%s" /></td></tr>
<tr><td class="header">Название</td><td><input type="text" name="title" value="%s" /></td></tr>
</table>
<input type="submit" value="Сохранить" />
</form>
<form action="/books/%s" method="POST">
<input type="hidden" name="_method" value="DELETE" />
<input type="submit" value="Удалить" />
</form>
<p></p>
<a href="/books">К списку книг</a>
</body>
</html>
"""
new_book_template = u"""
<!DOCTYPE html>
<html>
<head>
<title>Книга - Добавить</title>
<style type="text/css">.header {font-weight:bold;background-color:#cceecc;}</style>
</head>
<body>
<h2>Книга - Добавить</h2>
<form action="/books" method="POST">
<table>
<tr><td class="header">Автор</td><td><input type="text" name="author" /></td></tr>
<tr><td class="header">Название</td><td><input type="text" name="title" /></td></tr>
</table>
<input type="submit" value="Сохранить" />
</form>
<p></p>
<a href="/books">К списку книг</a>
</body>
</html>
"""
error404_template = u"""
<!DOCTYPE html>
<html>
<head>
<title>Список книг</title>
</head>
<body>
<h2>Такой страницы нет :(</h2>
</body>
</html>
"""
@app.route('/')
@app.route('/books')
def index():
text = ''
for key, val in books.items():
text += u'<tr><td><a href="/books/%s/edit">Edit</a></td><td><a href="/books/%s">%s</a></td><td>%s</td><td>%s</td></tr>' % (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/<int:id>/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/<int:id>', methods=['GET', 'POST']) # PUT and DELETE
def show(id):
if request.method == 'GET':
# /books/<int:id> 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/<int:id> 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/<int:id> 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!
Комментариев нет:
Отправить комментарий