Афонасьев Евгений
Обычно только во внутренних системах, например в административных панелях
https://developer.mozilla.org/ru/docs/Web/HTTP/Заголовки/Content-Type
https://developer.mozilla.org/ru/docs/Web/HTTP/Заголовки/Accept
@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'})
from flask import jsonify
@app.route('/_get_current_user')
def get_current_user():
return jsonify(
username=g.user.username,
email=g.user.email,
id=g.user.id,
)
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'
+ Легко начать
- Сложно объяснить пользователям как использовать апи
- Нет готовых стандартных инструментов
POST /api/rpc
User.create_user
User.add_friend
@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
users, pages, operations, actions
/users
/users?older=30
/users/<user_id>
/users/<user_id>/friends/<friend_id>
GET, POST, DELETE (PUT, PATCH)
400, 401, 403 etc.
GET /users - список пользователей
GET /users/<user_id> - конкретный пользователь
GET /users?page=5 - страница списка пользователей
Безопасный метод, никогда не должен приводить к изменениям данных на бэке!
POST /users {} - создание пользователя
POST /users/<user_id>/friends {} - добавление друга
Каждый вызов создает новую сущность!
Ожидает код 201
DELETE /users/<user_id> - удаление пользователя
Ожидает код 204 или 404
PUT /users/<user_id> {} - перетирание данных новыми
Нужно передать все поля сущности!
PUT /users/<user_id> {} - обновление данных
Можно передать только часть данных
@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 405 # Method not allowed
Добавление друга на REST
Важно понимать какие запросы можно повторять при ошибках!
Повторять можно 5** коды и ошибки соединения
Свойство объекта или операции при повторном применении операции к объекту давать тот же результат, что и при первом.
В контексте веб обычно подразумевается, что на один и тот же запрос будет одно и то же действие на сервере, и такой запрос можно безопасно повторять
Идемпотетности можно достигнуть, добавив ко всем запросам ключ идемпотентности с дальнейшем валидацией на стороне сервера
Это протокол передачи данных, который относит каждый запрос к независимой транзакции, которая не связана с предыдущим запросом
Такой подход позволяет строить хорошо масштабируемые/надежные системы
Удобно документировать апи, методы и ошибки HTTP достаточно понятны и очевидны.
Хорошо подходит для публичных API для сторонних разработчиков
Хорошо поддается дополнительной обработке на промежуточных узлах и кэшированию
Сложнее проектировать api, нужно придумывать как описать свою систему в виде набора ресурсов
Нет единого стандарта (как передавать ошибки, в каком формате передавать данные)
GET /users?limit=100&offset=200
GET /users?size=100&page=200
GET /users?id=20&limit=200
GET /users?ts=1635346346&page=200
{
"page": 10,
"count": 1000,
"result": [
{
"user_id": 1
},
...
]
}
[
{
"user_id": 1
},
...
]
# служебные данные в http headers
http://mypy-lang.org/
Инструмент статического анализа типов в python коде
def greeting(name: str) -> str:
return 'Hello ' + name
user: User = get_user(...)
class Container:
users: List[User]
...
In [1]: def x(i: int) -> int:
...: pass
...:
In [2]: x.__annotations__
Out[2]: {'i': int, 'return': int}
Union[int, str, float]
from typing import Optional
def greeting(name: str) -> Optional[str]:
if name:
return 'Hello ' + name
else:
return None
List[int]
Dict[str, int]
from typing import NoReturn
def inf_loop() -> NoReturn:
while True:
pass
user = cast(User, user)
from typing import List, Union
Vector = List[Union[int, float]]
def scale(scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]
from typing import Callable
def feeder(get_next_item: Callable[[], str]) -> None:
# Body
from typing import Sequence, TypeVar
# Declare type variable
T = TypeVar('T')
# Generic function
def first(l: Sequence[T]) -> T:
return l[0]
from typing import Any
class User: ...
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...
# Accepts User, BasicUser, ProUser, TeamUser, ...
def make_new_user(user_class: Type[User]) -> User:
# ...
return user_class()
# Generator[YieldType, SendType, ReturnType]
def echo_round() -> Generator[int, float, str]:
sent = yield 0
while sent >= 0:
sent = yield round(sent)
return 'Done'
def infinite_stream(
start: int
) -> Generator[int, None, None]:
while True:
yield start
start += 1
def infinite_stream(
start: int
) -> Iterator[int]:
while True:
yield start
start += 1
class Employee(NamedTuple):
name: str
id: int = 3
employee = Employee('Guido')
assert employee.id == 3
if TYPE_CHECKING:
import expensive_mod
def fun(arg: 'expensive_mod.SomeType') -> None:
pass
https://pydantic-docs.helpmanual.io
from datetime import datetime
from typing import List
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
signup_ts: datetime
friends: List[int]
external_data = {
'id': '123',
'signup_ts': '2019-06-01 12:22',
'friends': [1, 2, '3']
}
user = User(**external_data)
print(user.id)
print(repr(user.signup_ts))
print(user.friends)
print(user.dict())
from pydantic import ValidationError
try:
User(
signup_ts='broken',
friends=[1, 2, 'not number'],
)
except ValidationError as e:
print(e.json())
class Friend(BaseModel):
id: int
name: str
class User(BaseModel):
id: int
name: str
signup_ts: datetime
friends: List[Friend]
Простой, читаемый код
Автодополнение