Episode 7
Разработка API
@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'))
Обычно только во внутренних системах, например в административных панелях
@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...'
@app.route('/users')
def users():
return (
'user_id;name;surname;money\n'
'user1;igor;volkov;13;2000\n'
'user2;vanya;batkov;83;8000\n'
)
@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>
"""
Когда-то широко использовался и может до сих встретиться в различных системах разрабатываемых долгий срок
import json
@app.route('/data')
def data():
return json.dumps({'id': 1, 'name': 'name'})
import ujson
@app.route('/data')
def data():
return ujson.dumps({'id': 1, 'name': 'name'})
Работает быстрее встроенного json модуля
try:
import ujson as json
except ImportError:
import json
часто является опциональной зависимостью для библиотек (но его поведение не на 100% совпадает со стандартным модулем!)
import msgpack
@app.route('/data')
def data():
user = {'id': 1, 'name': 'name'}
return msgpack.packb(user, use_bin_type=True)
+ Объем передаваемых данных значительно меньше
- Больше нагрузка на CPU (обычно не критично)
- Все клиенты должны уметь распаковывать данные из используемого формата
- Нельзя понять, что лежит в сообщении, без декодирования
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;
}
+ Объем передаваемых данных значительно меньше
+ Встроенная валидация данных по схеме
+ Обычно позволяет изменять схему с сохранением обратной совместимости
- Больше нагрузка на CPU
- Нужно передавать схемы всем системам, которые должны будут работать с данными
- Нельзя понять, что лежит в сообщении, без декодирования
@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'
+ Легко начать
- Сложно объяснить пользователям как использовать апи
- Нет готовых стандартных инструментов
@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
# 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}
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()
+ Легко проектировать api, методы из кода напрямую ложатся на методы RPC
+ Можно использовать в качестве транспорта не только http (ws) без больших изменений в коде
+ Хорошо подходит для внутренних api внутри одной системы для взаимодействия между сервисами
- Описывать и документировать API все еще сложно, нужно пояснять смысл методов и придумывать им понятные названия
Экосистема - свои сервера, свои клиенты, свой формат сериализации.
Кодогенерация сервера и клиента.
Representational State Transfer
@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
В контексте веб обычно подразумевается, что на один и тот же запрос будет одно и то же действие на сервере и такой запрос можно безопасно повторять, ответ в некоторых случаях может отличаться (DELETE 204 or 404).
Такие методы как GET PUT DELETE считаются идемпотентными, а POST нет!
Все свое нашу с собой! В запросе должны быть все данные для его осуществления. Сервер не хранит состояние конкретного клиента на своей стороне и результат каждого запроса не зависит от предыдущих.
+ Удобно документировать апи, методы и ошибки HTTP достаточно понятны и очевидны.
+ Хорошо подходит для публичных API для сторонних разработчиков
+ Хорошо поддается дополнительной обработке на промежуточных узлах и кэшированию
- Сложнее проектировать api, нужно придумывать как описать свою систему в виде набора ресурсов
- Нет единого стандарта (как передавать ошибки, в каком формате передавать данные)
- Жестко привязан к HTTP протоколу
Если у нас есть множество сервисов проверять авторизацию на каждом из них накладно, поэтому можно каждый запрос верифицировать через отдельный auth сервер.
Сделать это можно как на уровне каждого сервиса, так и на уровне балансера (nginx)
Важно! Данные не зашифрованы и могут быть прочитаны на клиенте!
>>> import jwt
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.'
'eyJzb21lIjoicGF5bG9hZCJ9.'
'4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
{'some': 'payload'}
Участники вы, преподаватели и стажеры :)
Дата: 31 марта в 15:00 (продолжительность 2-3 часа);
Место: https://vk.com/bub_ekat
Обещаем, что точно будет весело и интересно!