FastApi: основы

Зачем нужно

FastApi — это фреймворк позволяющий создавать «самодокументирующиеся API». Т.е. и реализация и документация пишется прямо в коде. Причем FastApi при помощи вебинтерфейса предоставляет еще и возможность тестирования получившегося API.

Подготовка

Для работы потребуется установленные пакеты fastapi и uvicorn :

pip3 install fastapi
pip3 install uvicorn

uvicorn требуется чтобы запустить веб сервер на определенном порту, для взаимодействия с фреймворком fastapi. Далее его можно проксировать через apache или ngnix, В основном для того, чтобы к api можно было обращаться из вне не только по IP но и по DNS имени. Ну и чтобы обернуть в https. Хотя консольный uvicorn позволяет может запускаться и по https протоколу, если указать ему сертификаты. Но не пробовал, ибо было не нужно.

Еще разик акцентирую внимание: uvicorn может поставляться в виде пакета python или в виде консольной утилиты. По сути это два разных способа поднятия вебсервера для fastapi. Результат будет один. Только в случае использования пакета python — внутри скрипта нужно будет реализовывать запуск uvicorn

Первый шаг

Как происходит работа внешнего пользователя с API? Обычно он авторизуется по логину-паролю, получает «сессионный ключ» и далее работает уже с ним. Что собственно и попробуем реализовать. Сначала на минимальном примере.

В FastApi используется модель pedantic, т.е. на входе функций реализуемых API должно попадать данные согласно заранее построенной схеме. Вызов функции, осуществляется при помощи роутинга в URL.

Например реализуем вызов функции API авторизации по URL вида:

http://127.0.0.1:8000/v1/auth

Запрос к URL будет выполнятся при помощи POST, с параметрами login и password

Создадим следующую структуру файлов и папок:

В файле main.py реализуем запуск сервера uvicorn и начальные настройки для запуска фреймворка FastAPI.

В файле /app/routers/auth.py реализуем логику ответа на вызов URL с параметрами POST

В файле /app/schemas/auth.py — создадим проверку входящих и исходящих параметров вызова согласно схеме pedantic

main.py:

import uvicorn
from fastapi import FastAPI
from app.routers import auth

tags_metadata = [
    {"name": "auth", "description": "Получение ключа по логину, паролю"},
]

app = FastAPI(title="API documentation", description="Какое то описание ",version="0.0.1", openapi_tags=tags_metadata, )

app.include_router(auth.router, prefix="/v1")

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000, log_level="info")

Схема auth.py:

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

from typing import Optional
from pydantic import BaseModel,Field

class auth_out(BaseModel):
    """ Что Выходит """
    hash: str = Field(..., title="Сессионый ключ полученый по логину - паролю", example="iauwhfpseurh e")

class auth_in(BaseModel):
    """ Что входит """
    login: Optional [str] = Field(..., title="Логин пользователя", example="Например Вася")
    password: Optional [str] =Field(..., title="Пароль пользователя", example="Например IUHID#U$HI")

Роутер auth.py:

# -*- coding: utf-8 -*-
"""Different helper-functions to work with users."""
import json

from fastapi import APIRouter,Depends
from app.schemas import auth as auth_models

router = APIRouter()

@router.post('/auth',response_model=auth_models.auth_out,tags=["auth"]) # заданы исходящие параметры
async def auth(into = Depends(auth_models.auth_in)): # заданы входящие параметры
    # если параметры не заданы
    if into.password==None: into.password="NONE"
    if into.login == None: into.password = "NONE"
    hash="eopwhdfweruieruiopfhp[ruifhp[eruifhperf8yareuifh[ay8wedfper8"
    # сформируем ответ
    out=auth_models.auth_out(hash=hash)
    return out

Ну вот собственно и всё. Минимальный рабочий каркас собран. Осталось попробовать его запустить и посмотреть результат в браузере. Для этого в консоли запустим:

python main.py

Далее открываем браузер по url http://127.0.0.1:8000/docs или (http://127.0.0.1:8000/redoc) и можем наблюдать уже сформированную готовую к употреблению документацию:

Причем работоспособность вызовов можно попробовать прямо сейчас.

В текущем случае мы запустили сервер uvicorn из скрипта python, но есть вариант запуска сервера и из консоли. Какие преимущества имеет запуск из консоли, мне не очень пока очевидны. Но факт есть факт. Скрипт можно запустить и так:

uvicorn main:app --port 8000 --workers 10

В данном случае запустили 10 воркеров FastApi. Если использовать ключ —reload, то сервер будет отслеживать изменение файлов и перезапускать сам себя. Это удобно для моментальной отладки

Внимание! Ключи —reload и —workers одновременно не работают.

Python: Получение состояния реле Sonoff Basic R3

Задача: получить состояние реле и записать его состояние в БД

Решение:

Реле отдает своё состояние по ссылке:

http://{ip}:8081/zeroconf/info

Однако в зависимости от ревизии прошивки, данные могут немного отличаться. В нижеприведённом скрипте это учтено:

#!/usr/bin/python3
import config
import pymysql
import requests
from urllib3.exceptions import InsecureRequestWarning
import random

ip="192.168.88.245"             # IP реле
source=2        # источник - реле SonOff_1 (Гостинная)
place=1         # расположение реле - гостиннная

# здесь кусок кода который опрашивает текущее состояние реле
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
post_params = '{"deviceid": "","data": {}}'
response = requests.post(f"http://{ip}:8081/zeroconf/info", data=post_params, verify=False)
try:
    res = response.json()
    print(f"пришло:{res}")
    switch=True
    signal=0
    if type(res['data'])==str:
      res["data"]=json.loads(res["data"])
    if res["data"]["switch"]=="off":switch=False
    if "signalStrength" in res["data"]:signal=res["data"]["signalStrength"]

except Exception as e:
    print(f"Ошибка:{e}")
    exit(-1)

print(signal)
print(switch)

# соединяемся с БД
con=pymysql.connect(host=config.gconfig['mysql_host'],
                port=3306,
                user=config.gconfig['mysql_user'],
                password=config.gconfig['mysql_password'],
                database=config.gconfig['mysql_base'],
                cursorclass=pymysql.cursors.DictCursor
        )
# ложим данные в БД
with con.cursor() as cursor:
    sql=f"insert into m_data (place,source,value_type,value,dt) values ({place},{source},3,{switch},now())";
    cursor.execute(sql)
    con.commit()
    sql=f"insert into m_data (place,source,value_type,value,dt) values ({place},{source},4,{signal},now())";
    cursor.execute(sql)
    con.commit()

Ротация логов в приложении на Python

Задача: в приложении используется встроенный logging. Необходимо организовать ротацию логов со сжатием «архива»

Решение:

import coloredlogs,logging
from logging.handlers import TimedRotatingFileHandler
import gzip

class GZipRotator:
    def __call__(self, source, dest):
        os.rename(source, dest)
        f_in = open(dest, 'rb')
        f_out = gzip.open("%s.gz" % dest, 'wb')
        f_out.writelines(f_in)
        f_out.close()
        f_in.close()
        os.remove(dest)

rotation_logging_handler = TimedRotatingFileHandler(gconfig["logging_file"], when='D', interval=1, backupCount=5)
rotation_logging_handler.rotator=GZipRotator();
fh = logging.FileHandler('spam.log')
fh.filemode="a"

logger = logging.getLogger(__name__)
logging.basicConfig(format='%(asctime)s %(message)s',datefmt='%d.%m.%Y %H:%M:%S',level=gconfig["logging_level"],handlers=[rotation_logging_handler,fh])
logging.captureWarnings(True)

Результатом работы будет создание 1 раз в день сжатого файла

Python: из объекта в JSON

Если применять json.dumps с объектом, то можно словить ошибку сериализации объекта. Для обхода этого можно применить следующий способ:

def serialize(obj):    
    return obj.__dict__
loc=json.dumps(loc, default=serialize)

В этом случае, если штатный сериализатор «не справляется», то ему будет передана «подсказка» о типе

uvicorn: запускается только один инстанс при любом количестве workers

Может поможет страждущим как и я. Двое суток гугла и янденкса ;(

Во время отладки скрипта на FastApi возникла ситуация, что сервис должен обращаться сам к себе, ну типа небольшая рекруссия. Соответсвенно чтоб трюк сработал, нужно чтобы было запущено несколько инстансов uvicorn одновременно. Ну ноу проблем, запускаю:

uvicorn main:app --reload --port 8000 --workers 10

И нифига. Запускается только один инстанс. И так и сяк, и гул и яндекс.

Наутро доходит попробовать убрать —reload (ну т.е. перезапускать инстанс автоматически при изменении кода — очень полезная штука во время отладки). И…..заработало! Баг?

1 3 4 5 6 7 14