Tinkoff Python
Episode 7
Разработка API
WIFI
65539-03378
В предыдущей серии...
Серверный рендеринг
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946021/pasted-from-clipboard.png)
Серверный рендеринг
@app.route('/')
def index():
return render_template('index.html', tasks=tasks)
@app.route('/create', methods=['POST'])
def create():
description = request.form['description']
create_task(description)
return redirect(url_for('index'))
Редко используется в крупных проектах
Обычно только во внутренних системах, например в административных панелях
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945994/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945997/pasted-from-clipboard.png)
Почему?
- Интерфейс требует перезагрузки страницы и запросов на сервер на каждое действие
- ограниченность в разработке интерактивных интерфейсов
- нельзя написать нормальное мобильное приложение
- неудобно работать с ajax
Чтобы писать одновременно и мобильные приложения и веб сервер должен оперировать только данными
API - application programming interface
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946010/pasted-from-clipboard.png)
HTTP API состоит из эндпоинтов
- GET /api/v1/get_user
- GET /api/v1/get_user_list
- POST /api/v1/create_user
Грамотно написанное API позволяет использовать себя для разных платформ
Фронты разные, бэк один
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946005/pasted-from-clipboard.png)
Форматы передачи данных
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946013/pasted-from-clipboard.png)
Text
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945966/pasted-from-clipboard.png)
Text
@app.route('/user/<user_id>/name')
def user_name(user_id):
return 'name'
@app.route('/user/<user_id>/desc')
def user_desc(user_id):
return 'long text about user...'
csv
@app.route('/users')
def users():
return (
'user_id;name;surname;money\n'
'user1;igor;volkov;13;2000\n'
'user2;vanya;batkov;83;8000\n'
)
xml
@app.route('/data')
def data():
return """
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
"""
xml
Когда-то широко использовался и может до сих встретиться в различных системах разрабатываемых долгий срок
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945889/pasted-from-clipboard.png)
json
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945959/pasted-from-clipboard.png)
json
import json
@app.route('/data')
def data():
return json.dumps({'id': 1, 'name': 'name'})
json
- Современный стандарт
- Человеко-читаемые данные
- Удобно работать из js
- Компактнее чем xml
- Будет достаточен в 99% случаев
ujson
import ujson
@app.route('/data')
def data():
return ujson.dumps({'id': 1, 'name': 'name'})
Работает быстрее встроенного json модуля
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945858/pasted-from-clipboard.png)
ujson
try:
import ujson as json
except ImportError:
import json
часто является опциональной зависимостью для библиотек (но его поведение не на 100% совпадает со стандартным модулем!)
Бинарные форматы
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945866/pasted-from-clipboard.png)
msgpack
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945766/pasted-from-clipboard.png)
msgpack
import msgpack
@app.route('/data')
def data():
user = {'id': 1, 'name': 'name'}
return msgpack.packb(user, use_bin_type=True)
Бинарные форматы
+ Объем передаваемых данных значительно меньше
- Больше нагрузка на CPU (обычно не критично)
- Все клиенты должны уметь распаковывать данные из используемого формата
- Нельзя понять, что лежит в сообщении, без декодирования
Зачастую можно добиться схожих результатом с помощью gzip
Бинарные форматы со схемами
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945882/pasted-from-clipboard.png)
Protobuf
message Car {
required string model = 1;
enum BodyType {
sedan = 0;
hatchback = 1;
SUV = 2;
}
required BodyType type = 2 [default = sedan];
optional string color = 3;
required int32 year = 4;
message Owner {
required string name = 1;
required string lastName = 2;
required int64 driverLicense = 3;
}
repeated Owner previousOwner = 5;
}
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945810/pasted-from-clipboard.png)
Бинарные форматы со схемами
+ Объем передаваемых данных значительно меньше
+ Встроенная валидация данных по схеме
+ Обычно позволяет изменять схему с сохранением обратной совместимости
- Больше нагрузка на CPU
- Нужно передавать схемы всем системам, которые должны будут работать с данными
- Нельзя понять, что лежит в сообщении, без декодирования
Шаблоны построения API
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946156/pasted-from-clipboard.png)
Велосипеды
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946174/pasted-from-clipboard.png)
Велосипеды
@app.route('/get_users')
def get_users():
return json.dumps([
{'id': 1, 'name': 'name'},
{'id': 2, 'name': 'name'},
])
@app.route('/user/<user_id>')
def get_user(user_id):
user = db.get_user(user_id)
return json.dumps(user)
@app.route('/create_user')
def create_user(user_id):
user = db.create_user(request.data)
return 'ok'
Плюсы/минусы
+ Легко начать
- Сложно объяснить пользователям как использовать апи
- Нет готовых стандартных инструментов
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946182/pasted-from-clipboard.png)
RPC
- Все запросы идут на один эндпоинт
- Оперирует объектами и методами
- Использует http только как транспорт (только post запросы, только 200 code status в ответах)
Можно написать что-то свое
@app.route('/api', methods=['POST'])
def handle_request():
content = request.json
if content['method'] == 'User.add_friend':
User.add_friend(**content['kwargs'])
if content['method'] == 'User.get_friends':
friends = User.get_friends(**content['kwargs'])
return friends, 200
return 200
Но лучше использовать стандарты!
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946977/pasted-from-clipboard.png)
XML-RPC - устарел
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5945889/pasted-from-clipboard.png)
JSON RPC
# normal request
--> {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23}, "id": 3}
<-- {"jsonrpc": "2.0", "result": 19, "id": 3}
# error
--> []
<-- {"jsonrpc": "2.0", "error": {"code": -32, "message": "Invalid Request"}, "id": null}
JSON RPC
from flask import Flask, request, Response
from jsonrpcserver import method, dispatch
app = Flask(__name__)
@method
def ping():
return "pong"
@app.route("/", methods=["POST"])
def index():
req = request.get_data().decode()
response = dispatch(req)
return Response(str(response), response.http_status, mimetype="application/json")
if __name__ == "__main__":
app.run()
Какие методы мы бы написали для корзины покупок?
Корзина покупок
- User.add_item(user_id, item_id) - все товары в корзине
- User.add_item(user_id, item_id) - добавить товар
- Item.get_info(item_id) - информация о конкретном товаре
- User.del_item(user_id, item_id) - удалить товар
- User.buy() - купить все, что в корзине
Плюсы/минусы
+ Легко проектировать api, методы из кода напрямую ложатся на методы RPC
+ Можно использовать в качестве транспорта не только http (ws) без больших изменений в коде
+ Хорошо подходит для внутренних api внутри одной системы для взаимодействия между сервисами
- Описывать и документировать API все еще сложно, нужно пояснять смысл методов и придумывать им понятные названия
gRPC
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946391/pasted-from-clipboard.png)
gRPC
Экосистема - свои сервера, свои клиенты, свой формат сериализации.
Кодогенерация сервера и клиента.
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946352/pasted-from-clipboard.png)
Representational State Transfer
REST
- API оперирует ресурсами
- Каждый ресурс имеет свой уникальный url
- Операции кодируются HTTP методами (get, post, put, delete, etc.)
- Ошибки отдаются с использованием HTTP status codes (400, etc.), а подробное описание ошибки в теле ответа в json
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946362/pasted-from-clipboard.png)
- Имя ресурса во множественно числе (/users)
- GET /users - вернет список всех объектов
- POST /users с json в теле запроса - создаст новый объект
- GET /users/<user_id> - вернет объект
- DELETE /users/<user_id> - удалит объект
- PUT /users/<user_id> с json в теле запроса - обновит объект переданными данными (в теле запроса должны быть все поля!)
@app.route('/api', methods=['POST'])
def handle_request():
content = request.json
if content['method'] == 'User.add_friend':
User.add_friend(**content['kwargs'])
if content['method'] == 'User.get_friends':
return User.get_friends(**content['kwargs'])
return 200
Добавление друга на RPC
@app.route('/api/users/<user_id>/friends', methods=['GET', 'POST'])
def handle_request(user_id):
if request.method == 'POST':
User.add_friend(user_id, friend=request.json)
return friend, 201 # Created
if request.method == 'GET':
friends = User.get_friends(user_id)
return friends, 200 # Ok
return 400 # Bad Request
Добавление друга на REST
Какие методы мы бы написали для корзины покупок?
Корзина покупок
- GET /users/1/bucket/items - все товары в корзине
- POST /users/1/bucket/items - добавить товар
- GET /users/1/bucket/items/<item_id> - информация о конкретном товаре
- DELETE /users/1/bucket/items/<item_id> - удалить товар
- POST /users/1/purchase создать покупку
Идемпотентность
В контексте веб обычно подразумевается, что на один и тот же запрос будет одно и то же действие на сервере и такой запрос можно безопасно повторять, ответ в некоторых случаях может отличаться (DELETE 204 or 404).
Такие методы как GET PUT DELETE считаются идемпотентными, а POST нет!
stateless
Все свое нашу с собой! В запросе должны быть все данные для его осуществления. Сервер не хранит состояние конкретного клиента на своей стороне и результат каждого запроса не зависит от предыдущих.
Плюсы/минусы
+ Удобно документировать апи, методы и ошибки HTTP достаточно понятны и очевидны.
+ Хорошо подходит для публичных API для сторонних разработчиков
+ Хорошо поддается дополнительной обработке на промежуточных узлах и кэшированию
- Сложнее проектировать api, нужно придумывать как описать свою систему в виде набора ресурсов
- Нет единого стандарта (как передавать ошибки, в каком формате передавать данные)
- Жестко привязан к HTTP протоколу
Перерыв?
swagger
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946826/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946822/pasted-from-clipboard.png)
Может быть автоматически сгенерирован!
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946435/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946923/pasted-from-clipboard.png)
Аутентификация
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946477/pasted-from-clipboard.png)
только HTTPS!
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946914/pasted-from-clipboard.png)
Аутентификация через заголовки Basic Auth
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946470/pasted-from-clipboard.png)
Проблемы
- Нужно везде таскать логин пароль в открытом виде
- Нужно хранить логин/пароль на стороне клиента
- Смена пароля потребует повторной аутентификации во всех системах
Аутентификация с помощью токена
- Передаем в заголовках только токен доступа.
- Можно отозвать конкретный токен в случае, если он скомпрометирован.
- Можно указать время жизни отдельного токена, после которого придется получать новый.
- Токен выдается при аутентификации по логину и паролю или генерируется по запросу авторизованного пользователя
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5947120/pasted-from-clipboard.png)
Аутентификация третьей стороной
Если у нас есть множество сервисов проверять авторизацию на каждом из них накладно, поэтому можно каждый запрос верифицировать через отдельный auth сервер.
Сделать это можно как на уровне каждого сервиса, так и на уровне балансера (nginx)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5947130/pasted-from-clipboard.png)
OAuth2
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946447/pasted-from-clipboard.png)
Проблема токена в том, что нужно где-то хранить его привязку к конкретному пользователю (например чтобы получить user_id)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946522/pasted-from-clipboard.png)
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946525/pasted-from-clipboard.png)
Важно! Данные не зашифрованы и могут быть прочитаны на клиенте!
>>> import jwt
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
'eyJzb21lIjoicGF5bG9hZCJ9.'
'4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
{'some': 'payload'}
JWT Example
Преимущества
- Можно хранить полезную информацию
- Можно не проверять аутентификацию, пока токен не устареет
- Нельзя подделать
Не храним в payload`е важные приватные данные, они могут быть легко прочитаны клиентом!
Чтобы не вводить логин/пароль каждый раз, когда токен устареет, используется схема с двумя токенами:
access - на доступ, передается с каждым запросом
refresh - на получение новой пары токенов, когда старый access токен устареет
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5946882/pasted-from-clipboard.png)
refresh токены нужно хранить на сервере, чтобы иметь возможность отозвать (для разных устройств свои)
Что? Где? Когда?
Участники вы, преподаватели и стажеры :)
Дата: 31 марта в 15:00 (продолжительность 2-3 часа);
Место: https://vk.com/bub_ekat
Обещаем, что точно будет весело и интересно!
Вопросы?
Tinkoff Python 7
By Afonasev Evgeniy
Tinkoff Python 7
- 692