Русский
Русский
English
Статистика
Реклама

Fastapi

Перевод Разработчик популярного веб-фреймворка FastAPI об истории его создания и перспективах аннотаций типов Python

16.06.2021 12:16:05 | Автор: admin


Python-девелопер и писатель Рики Уайт взял интервью у Себастьяна Рамиреса, разработчика из Explosion AI. Но Себастьян не просто разработчик, это заметная фигура в open source сообществе, создатель популярных фреймворков FastAPI и Typer. В основном речь шла про широкие возможности применения аннотаций типов Python, историю создания фреймворка FastAPI и его дальнейшее развитие. Кроме того, Себастьян рассказал о своих планах по работе над другими open source проектами. Без лишних слов, давайте перейдем к интервью.

Рики: Спасибо, что пришёл, Себастьян. Сначала я бы хотел задать тебе те же вопросы, что и другим своим гостям. Как ты начал программировать? Когда познакомился с Python?

Себастьян: Спасибо, что пригласил [улыбается].

Я начал программировать, когда мне было пятнадцать. Я пытался создать веб-сайт для бизнеса своих родителей. Первым моим настоящим кодом был JavaScript внутри HTML модальное диалоговое окно (alert) с фразой Hello World. Я до сих пор помню, как обрадовался, увидев это маленькое окно с сообщением, и испытал чувство всемогущества от мысли, что это запрограммировал я.

Я много лет боялся изучать какой-либо другой язык, думая, что сначала должен хотя бы освоить JavaScript. Но потом на одном из многих онлайн-курсов, которые я проходил, возникла необходимость использовать Python для управления искусственным интеллектом в Pac-Man и для некоторых других задач. Курс состоял из одного длинного туториала по основам Python, и этого было достаточно. Мне очень хотелось попробовать.

Я быстро влюбился в Python и пожалел, что не начал раньше!

Рики: На сегодняшний день мне известно [поправь меня, если ошибаюсь], что ты трудишься в Explosion AI, компании, создавшей популярную платформу обработки естественного языка (NLP-библиотеку spaCy). Расскажи немного о трудовых буднях. Какие задачи в сфере искусственного интеллекта и машинного обучения интересуют команду и какие инструменты создала компания, чтобы помочь разработчикам быстрее продвигаться в обеих областях?

Себастьян: Да, Explosion в основном известен благодаря spaCy. Это NLP-библиотека с открытым исходным кодом. Они также создали Prodigy, коммерческий инструмент с поддержкой скриптования для эффективного аннотирования наборов данных в машинном обучении. Я работал в основном в Prodigy Teams. Это облачная версия Prodigy для совместного использования. Поскольку продукт ориентирован на конфиденциальность, создание облачной версии было связано с множеством особых проблем.

Тем не менее недавно я решил покинуть компанию. Теперь я планирую найти способ посвятить большую часть своего рабочего времени FastAPI, Typer и другим моим open source проектам. А ещё я, скорее всего, буду консультировать другие команды и компании.

Рики: Ты более известен как разработчик FastAPI, высокопроизводительной веб-платформы для создания API-интерфейсов, которая быстро стала одной из самых популярных в Python-сообществе. Что вдохновило тебя на его создание и как планируешь развивать его дальше? И вопрос от тех, кто ещё не пробовал FastAPI: почему можно смело использовать его в своём следующем проекте вместо других популярных фреймворков?

Себастьян: На самом деле я годами откладывал создание нового фреймворка.

Сначала я изучал и использовал множество фреймворков, плагинов и инструментов, постоянно совершенствуя свой воркфлоу (и воркфлоу команд, которые я возглавлял), одновременно улучшая создаваемые нами продукты. И так как я работал над стартапами с постоянными пивотами и ускоренными темпами разработки, у меня была возможность итеративно повторять поиск правильного техпроцесса и [наиболее удобных и эффективных] инструментов, комбинируя существующие фреймворки с большим количеством плагинов и тулзов.

У меня также была возможность поработать с другими технологиями с JavaScript и TypeScript на фронте, несколькими фреймворками для гибридных приложений, с Electron для десктопа и так далее. Тем не менее, большинство моих проектов (или даже все) были связаны с данными (data science, ML и так далее).

В какой-то момент у меня в голове сформировалось сочетание фич, которые мне нравились. Я взял их из разных фреймворков, инструментов и даже из других языков и экосистем:

  1. Автозаполнение в редакторе кода.
  2. Автоматическое обнаружение ошибок в редакторе (проверка типов).
  3. Возможность писать простой код.
  4. Автоматическая валидация данных.
  5. Автоматическое преобразование данных (сериализация).
  6. Автоматическая генерация документации для API.
  7. Поддержка стандартов OpenAPI для Web API, OAuth 2.0 для аутентификации и авторизации и JSON Schema для документирования.
  8. Внедрение зависимостей для упрощения кода и повторного использования кода в качестве утилит.
  9. Хорошая производительность / параллелизм.

Но я не мог объединить эти фичи, тупо интегрируя всё со всем. Идеальный воркфлоу, который прекрасно работал в моей голове, по-прежнему сталкивался с кучей проблем, когда дело доходило до реализации. Требовалась сложная интеграция плагинов, устаревших компонентов, недокументированных инструментов, не говоря про некрасивый избыточный код и прочие головняки.

В какой-то момент мой очередной эксперимент по скрещиванию опять провалился. Я как будто перепробовал всё, что можно, и задумался о том, что делать дальше. И тогда я понял: Час Х настал. Я стал изучать стандарты OpenAPI, JSON Schema, OAuth 2.0 и многие другие. Затем я начал реализовывать свои идеи именно так, как это работало у меня в голове. Сначала тестировал на нескольких редакторах, оптимизируя взаимодействие с разработчиками, и только потом закреплял это во внутренней логике моего проекта.

Затем я убедился, что у меня были подходящие строительные блоки для создания FastAPI: Starlette (для всех веб-частей) и pydantic (для работы с данными) оба имеют отличную производительность и фукнциональность. И, наконец, я приступил к реализации своего проекта с учётом стандартов, собранной обратной связи от разработчиков, а также некоторых дополнений (например, системы внедрения зависимостей).

У меня на сайте гораздо больше информации об истории создания, архитектуре и планах FastAPI, а также об альтернативах, источниках вдохновения и референсах.

Те, кому интересно попробовать, обычно начинают с главной страницы или README и далее следуют мини-руководству. Через пятнадцать-двадцать минут у них складывается представление о том, как работает FastAPI. После этого уже можно решить, нравится или нет, будет ли это полезно.

Ремарка для тех, кто уже использует другие фреймворки в своём проекте: не спешите переходить на FastAPI только потому, что он классный. Если ваш продукт работает хорошо и вам не нужно расширять функциональность, или вам не нужны преимущества FastAPI, то переходить на него не стоит.

А вообще, мигрировать на FastAPI относительно просто, там нет сложных интеграций. Вы можете использовать обычные Python-пакеты непосредственно в сочетании с FastAPI. Мигрировать можно постепенно или создавать с FastAPI только новые компоненты.

Рики: Ещё ты создал Typer, фреймворк, работающий в режиме командной строки (CLI). Он, так же как и FastAPI, во многом опирается на аннотации типов Python. Кажется, я замечаю закономерность [улыбается]. Что такого особенного в этой типизации, которая тебе так нравится? Считаешь ли ты, что всё больше библиотек должны использовать аннотации типов Python?

Себастьян: Да, конечно! Аннотации типов позволяют реализовать автозаполнение и проверку типов в редакторе кода. Аннотации типов, собственно, и были придуманы для этих задач.

Это особенно актуально, если вы создаёте инструмент, который, как ожидается, будут использовать другие разработчики. Я бы хотел, чтобы многие Python-пакеты, например, для инфраструктуры API или SaaS-клиентов, поддерживали аннотации типов. Это значительно улучшило бы жизнь их разработчиков и упростило бы внедрение инструментов.

Но теперь, благодаря pydantic, FastAPI и Typer, мы выяснили, что аннотации типов уже содержат много информации, которую можно использовать для создания более мощных инструментов. Разумеется, они самостоятельно могут обращаться к документации и выдавать для нас сообщения, наподобие ожидается, что имя будет строкой или ожидается, что возраст будет числом с плавающей точкой.

Аннотациии типов также можно использовать для валидации данных например, когда функция ожидает строку, но получает словарь. Это ошибка, о которой можно сообщить, и если фреймворк (например, FastAPI или Typer) состоит из набора таких функций с параметрами, он сам в состоянии выполнить такую валидацию.

Ещё аннотации типов можно использовать для сериализации данных. Например, в URL-адресе всё является строкой, и то же самое справедливо для CLI. Но если в аннотациях типов мы укажем, что нам нужно целое число, инфраструктура (FastAPI или Typer) может попытаться преобразовать, например, строку 42 из URL-адреса или командной строки в целое число.

Итак, с помощью интуитивно понятного и простого кода, который всего лишь описывает тип переменной, мы можем обеспечить работу трёх дополнительных функций (валидация данных, сериализация и документация), опираясь на базовую функциональность (автозаполнение и проверка типа). Так мы получим очень ценные плюшки, приложив достаточно мало усилий по написанию кода.

И в подавляющем большинстве случаев для всех этих функций требуется одна и та же информация по типу возраст это целое число. Таким образом, повторно используя один и тот же фрагмент кода (аннотацию типа), мы можем избежать его дублирования. Поддерживая единый источник истины (Single Source of Truth, SSOT), мы убережём себя от ошибок в будущем, если решим изменить тип в каком-то месте (например, для валидации данных), но забудем обновить его в другом месте (например, в документации).

Всё это из коробки могут делать FastAPI и Typer.

Рики: Каковы твои планы на будущее? Над какими ещё проектами будешь работать?

Себастьян: О да, у меня много планов. Может быть, даже слишком много [улыбается].

Есть несколько фич, которые я хочу добавить в FastAPI и Typer. Я также планирую поработать над автоматизацией UI администратора FastAPI, который не будет зависеть от базы данных (буду делать на основе OpenAPI). Ещё хочу интегрировать pydantic с SQLAlchemy для тех случаев, когда нужно общаться с базами данных (опять же, хочу воспользоваться преимуществами аннотаций типов и уменьшить дублирование кода).

Далее планирую улучшить, упростить и подробнее документировать все утилиты, связанные с OAuth 2.0, скоупом, сторонней аутентификацией и так далее.

Кроме того, хочу сделать больше контента об изучении всех этих штук, снять несколько видеороликов, возможно, сделать курс

Рики: Теперь, задам тебе, пожалуй, два последних вопроса. Чем ещё занимаешься в свободное время? Чем интересуешься, помимо Python и программирования?

Себастьян: В последнее время у меня не было возможности отвлекаться на другие занятия, но я надеюсь, в дальнейшем это немного изменится. Когда у меня есть время, я люблю играть в видеоигры с женой (а иногда и один), смотреть фильмы, завтракать или пить кофе с друзьями где-нибудь в Берлине (когда нет карантина).

Мне очень нравится работать над своими open source проектами, поэтому я легко могу сидеть за этим делом часами даже в выходные дни и не замечать, как проходит время [улыбается].

Рики: Спасибо, что пришёл, Себастьян. Классно пообщались!

Себастьян: Всегда рад.Большое спасибо за приглашение!



VPS серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Пишем веб сервис на Python с помощью FastAPI

04.08.2020 08:14:31 | Автор: admin
image
Знаю, знаю, наверное вы сейчас думаете что опять?!.
Да, на хабре уже неоднократно писали о фреймворке FastAPI. Но я предлагаю рассмотреть этот инструмент немного подробнее и написать API своего собственного мини Хабра без кармы и рейтингов, зато с блэкджеком и с тестами, аутентификацией, миграциями и асинхронной работой с БД.

Схема базы данных и миграции


Прежде всего, с помощью SQLAlchemy Expression Language, опишем схему базы данных. Создадим файл models/users.py:
import sqlalchemyfrom sqlalchemy.dialects.postgresql import UUIDmetadata = sqlalchemy.MetaData()users_table = sqlalchemy.Table(    "users",    metadata,    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),    sqlalchemy.Column("email", sqlalchemy.String(40), unique=True, index=True),    sqlalchemy.Column("name", sqlalchemy.String(100)),    sqlalchemy.Column("hashed_password", sqlalchemy.String()),    sqlalchemy.Column(        "is_active",        sqlalchemy.Boolean(),        server_default=sqlalchemy.sql.expression.true(),        nullable=False,    ),)tokens_table = sqlalchemy.Table(    "tokens",    metadata,    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),    sqlalchemy.Column(        "token",        UUID(as_uuid=False),        server_default=sqlalchemy.text("uuid_generate_v4()"),        unique=True,        nullable=False,        index=True,    ),    sqlalchemy.Column("expires", sqlalchemy.DateTime()),    sqlalchemy.Column("user_id", sqlalchemy.ForeignKey("users.id")),)

И файл models/posts.py:
import sqlalchemyfrom .users import users_tablemetadata = sqlalchemy.MetaData()posts_table = sqlalchemy.Table(    "posts",    metadata,    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),    sqlalchemy.Column("user_id", sqlalchemy.ForeignKey(users_table.c.id)),    sqlalchemy.Column("created_at", sqlalchemy.DateTime()),    sqlalchemy.Column("title", sqlalchemy.String(100)),    sqlalchemy.Column("content", sqlalchemy.Text()),)

Чтобы автоматизировать миграции базы данных, установим alembic:
$ pip install alembic

Для инициализации Alembic выполним:
$ alembic init migrations

Эта команда создаст в текущей директории файл alembic.ini и каталог migrations содержащий
  • каталог versions, в котором будут хранится файлы миграций
  • скрипт env.py, запускающийся при вызове alembic
  • файл script.py.mako, содержащий шаблон для новых миграций.


Укажем url нашей базы данных, для этого в файле alembic.ini добавим строчку:
sqlalchemy.url = postgresql://%(DB_USER)s:%(DB_PASS)s@%(DB_HOST)s:5432/%(DB_NAME)s

Формат %(variable_name)s позволяет нам устанавливать разные значения переменных в зависимости от среды окружения, переопределяя их в файле env.py например вот так:

from os import environfrom alembic import contextfrom app.models import posts, users# Alembic Config объект предоставляет доступ# к переменным из файла alembic.iniconfig = context.configsection = config.config_ini_sectionconfig.set_section_option(section, "DB_USER", environ.get("DB_USER"))config.set_section_option(section, "DB_PASS", environ.get("DB_PASS"))config.set_section_option(section, "DB_NAME", environ.get("DB_NAME"))config.set_section_option(section, "DB_HOST", environ.get("DB_HOST"))fileConfig(config.config_file_name)target_metadata = [users.metadata, posts.metadata]

Здесь мы берем значения DB_USER, DB_PASS, DB_NAME и DB_HOST из переменных окружения. Кроме этого, в файле env.py указываются метаданные нашей базы в атрибуте target_metadata, без этого Alembic не сможет определить какие изменения необходимо произвести в базе данных.

Все готово и мы можем сгенерировать миграции и обновить БД:
$ alembic revision --autogenerate -m "Added required tables"$ alembic upgrade head


Запускаем приложение и подключаем БД


Создадим файл main.py:
from fastapi import FastAPIapp = FastAPI()@app.get("/")def read_root():    return {"Hello": "World"}

И запустим приложение, выполнив команду
$ uvicorn main:app --reload

Убедимся, что все работает как надо. Открываем в браузере http://127.0.0.1:8000/ и видим
{"Hello": "World"}

Чтобы подключиться к базе данных, воспользуемся модулем databases, который позволяет выполнять запросы асинхронно.
Настроим startup и shutdhown события нашего сервиса, при которых будут происходить подключение и отключение от базы данных. Отредактируем файл main.py:
from os import environimport databases# берем параметры БД из переменных окруженияDB_USER = environ.get("DB_USER", "user")DB_PASSWORD = environ.get("DB_PASSWORD", "password")DB_HOST = environ.get("DB_HOST", "localhost")DB_NAME = "async-blogs"SQLALCHEMY_DATABASE_URL = (    f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}")# создаем объект database, который будет использоваться для выполнения запросовdatabase = databases.Database(SQLALCHEMY_DATABASE_URL)app = FastAPI()@app.on_event("startup")async def startup():    # когда приложение запускается устанавливаем соединение с БД    await database.connect()@app.on_event("shutdown")async def shutdown():    # когда приложение останавливается разрываем соединение с БД    await database.disconnect()@app.get("/")def read_root():    # изменим роут таким образом, чтобы он брал данные из БД    query = (        select(            [                posts_table.c.id,                posts_table.c.created_at,                posts_table.c.title,                posts_table.c.content,                posts_table.c.user_id,                users_table.c.name.label("user_name"),            ]        )        .select_from(posts_table.join(users_table))        .order_by(desc(posts_table.c.created_at))    )    return await database.fetch_all(query)

Открываем http://127.0.0.1:8000/ и если видим в ответе пустой список [], значит все прошло хорошо и можно двигаться дальше.

Валидация запроса и ответа


Реализуем возможность регистрации пользователей. Для этого нам понадобиться валидировать HTTP запросы и ответы. Для решения этой задачи воспользуемся библиотекой pydantic:
pip install pydantic

Создадим файл schemas/users.py и добавим модель, отвечающую за валидацию тела запроса:
from pydantic import BaseModel, EmailStrclass UserCreate(BaseModel):    """ Проверяет sign-up запрос """    email: EmailStr    name: str    password: str

Обратите внимание, что типы полей определяются с помощью аннотации типов. Помимо встроенных типов данных, таких как int и str, pydantic предлагает большое количество типов, обеспечивающих дополнительную проверку. Например, тип EmailStr проверяет, что полученное значение корректный email. Для использования типа EmailStr необходимо установить модуль email-validator:
pip install email-validator

Тело ответа должно содержать свои собственные специфические поля, например id и access_token, поэтому добавим в файл schemas/users.py модели, отвечающие за формирование ответа:
from typing import Optionalfrom pydantic import UUID4, BaseModel, EmailStr, Field, validatorclass UserCreate(BaseModel):    """ Проверяет sign-up запрос """    email: EmailStr    name: str    password: strclass UserBase(BaseModel):    """ Формирует тело ответа с деталями пользователя """    id: int    email: EmailStr    name: strclass TokenBase(BaseModel):    token: UUID4 = Field(..., alias="access_token")    expires: datetime    token_type: Optional[str] = "bearer"    class Config:        allow_population_by_field_name = True    @validator("token")    def hexlify_token(cls, value):        """ Конвертирует UUID в hex строку """        return value.hexclass User(UserBase):    """ Формирует тело ответа с деталями пользователя и токеном """    token: TokenBase = {}

Для каждого поля модели можно написать кастомный валидатор. Например, hexlify_token преобразует UUID значение в hex строку. Стоит отметить, что вы можете использовать класс Field, когда нужно переопределить стандартное поведение поля модели. Например, token: UUID4 = Field(..., alias=access_token) устанавливает псевдоним access_token для поля token. Для обозначения, что поле обязательно, в качестве первого параметра передается специальное значение ... (ellipsis).

Добавим файл utils/users.py, в котором создадим методы, необходимые для записи пользователя в БД:
import hashlibimport randomimport stringfrom datetime import datetime, timedeltafrom sqlalchemy import and_from app.models.database import databasefrom app.models.users import tokens_table, users_tablefrom app.schemas import users as user_schemadef get_random_string(length=12):    """ Генерирует случайную строку, использующуюся как соль """    return "".join(random.choice(string.ascii_letters) for _ in range(length))def hash_password(password: str, salt: str = None):    """ Хеширует пароль с солью """    if salt is None:        salt = get_random_string()    enc = hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), 100_000)    return enc.hex()def validate_password(password: str, hashed_password: str):    """ Проверяет, что хеш пароля совпадает с хешем из БД """    salt, hashed = hashed_password.split("$")    return hash_password(password, salt) == hashedasync def get_user_by_email(email: str):    """ Возвращает информацию о пользователе """    query = users_table.select().where(users_table.c.email == email)    return await database.fetch_one(query)async def get_user_by_token(token: str):    """ Возвращает информацию о владельце указанного токена """    query = tokens_table.join(users_table).select().where(        and_(            tokens_table.c.token == token,            tokens_table.c.expires > datetime.now()        )    )    return await database.fetch_one(query)async def create_user_token(user_id: int):    """ Создает токен для пользователя с указанным user_id """    query = (        tokens_table.insert()        .values(expires=datetime.now() + timedelta(weeks=2), user_id=user_id)        .returning(tokens_table.c.token, tokens_table.c.expires)    )    return await database.fetch_one(query)async def create_user(user: user_schema.UserCreate):    """ Создает нового пользователя в БД """    salt = get_random_string()    hashed_password = hash_password(user.password, salt)    query = users_table.insert().values(        email=user.email, name=user.name, hashed_password=f"{salt}${hashed_password}"    )    user_id = await database.execute(query)    token = await create_user_token(user_id)    token_dict = {"token": token["token"], "expires": token["expires"]}    return {**user.dict(), "id": user_id, "is_active": True, "token": token_dict}


Создадим файл routers/users.py и добавим sign-up роут, указав, что в запросе он ожидает модель CreateUser и возвращает модель User:
from fastapi import APIRouterfrom app.schemas import usersfrom app.utils import users as users_utilsrouter = APIRouter()@router.post("/sign-up", response_model=users.User)async def create_user(user: users.UserCreate):    db_user = await users_utils.get_user_by_email(email=user.email)    if db_user:        raise HTTPException(status_code=400, detail="Email already registered")    return await users_utils.create_user(user=user)


Осталось только подключить роуты из файла routers/users.py. Для этого добавим в main.py следующие строки:
from app.routers import usersapp.include_router(users.router)


Аутентификация и контроль доступа


Теперь, когда в нашей базе данных есть пользователи, все готово для того чтобы настроить аутентификацию приложения. Добавим эндпоинт, который принимает имя пользователя и пароль и возвращает токен. Обновим файл routers/users.py, добавив в него:
from fastapi import Dependsfrom fastapi.security import OAuth2PasswordRequestForm@router.post("/auth", response_model=users.TokenBase)async def auth(form_data: OAuth2PasswordRequestForm = Depends()):    user = await users_utils.get_user_by_email(email=form_data.username)    if not user:        raise HTTPException(status_code=400, detail="Incorrect email or password")    if not users_utils.validate_password(        password=form_data.password, hashed_password=user["hashed_password"]    ):        raise HTTPException(status_code=400, detail="Incorrect email or password")    return await users_utils.create_user_token(user_id=user["id"])

При этом, нам не нужно самостоятельно описывать модель запроса, Fastapi предоставляет специальный dependency класс OAuth2PasswordRequestForm, который заставляет роут ожидать два поля username и password.

Чтобы ограничить доступ к определенным роутам для неаутентифицированных пользователей, напишем метод-зависимость(dependency). Он проверит, что предоставленный токен принадлежит активному пользователю и вернет данные пользователя. Это позволит нам использовать информацию о пользователе во всех роутах, требующих аутентификации. Создадим файл utils/dependecies.py:
from app.utils import users as users_utilsfrom fastapi import Depends, HTTPException, statusfrom fastapi.security import OAuth2PasswordBeareroauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth")async def get_current_user(token: str = Depends(oauth2_scheme)):    user = await users_utils.get_user_by_token(token)    if not user:        raise HTTPException(            status_code=status.HTTP_401_UNAUTHORIZED,            detail="Invalid authentication credentials",            headers={"WWW-Authenticate": "Bearer"},        )    if not user["is_active"]:        raise HTTPException(            status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user"        )    return user


Обратите внимание, что зависимость может в свою очередь зависеть от другой зависимости. К пример OAuth2PasswordBearer зависимость, которая дает понять FastAPI, что текущий роут требует аутентификации.

Чтобы проверить, что все работает как надо, добавим роут /users/me, возвращающий детали текущего пользователя. В файл routers/users.py добавим строки:
from app.utils.dependencies import get_current_user@router.get("/users/me", response_model=users.UserBase)async def read_users_me(current_user: users.User = Depends(get_current_user)):    return current_user

Теперь у нас есть роут /users/me к которому имеют доступ только аутентифицированные пользователи.

Все готово для того, чтобы наконец добавить возможность пользователям создавать и редактировать публикации:
utils/posts.py
from datetime import datetimefrom app.models.database import databasefrom app.models.posts import posts_tablefrom app.models.users import users_tablefrom app.schemas import posts as post_schemafrom sqlalchemy import desc, func, selectasync def create_post(post: post_schema.PostModel, user):    query = (        posts_table.insert()        .values(            title=post.title,            content=post.content,            created_at=datetime.now(),            user_id=user["id"],        )        .returning(            posts_table.c.id,            posts_table.c.title,            posts_table.c.content,            posts_table.c.created_at,        )    )    post = await database.fetch_one(query)    # Convert to dict and add user_name key to it    post = dict(zip(post, post.values()))    post["user_name"] = user["name"]    return postasync def get_post(post_id: int):    query = (        select(            [                posts_table.c.id,                posts_table.c.created_at,                posts_table.c.title,                posts_table.c.content,                posts_table.c.user_id,                users_table.c.name.label("user_name"),            ]        )        .select_from(posts_table.join(users_table))        .where(posts_table.c.id == post_id)    )    return await database.fetch_one(query)async def get_posts(page: int):    max_per_page = 10    offset1 = (page - 1) * max_per_page    query = (        select(            [                posts_table.c.id,                posts_table.c.created_at,                posts_table.c.title,                posts_table.c.content,                posts_table.c.user_id,                users_table.c.name.label("user_name"),            ]        )        .select_from(posts_table.join(users_table))        .order_by(desc(posts_table.c.created_at))        .limit(max_per_page)        .offset(offset1)    )    return await database.fetch_all(query)async def get_posts_count():    query = select([func.count()]).select_from(posts_table)    return await database.fetch_val(query)async def update_post(post_id: int, post: post_schema.PostModel):    query = (        posts_table.update()        .where(posts_table.c.id == post_id)        .values(title=post.title, content=post.content)    )    return await database.execute(query)


routers/posts.py
from app.schemas.posts import PostDetailsModel, PostModelfrom app.schemas.users import Userfrom app.utils import posts as post_utilsfrom app.utils.dependencies import get_current_userfrom fastapi import APIRouter, Depends, HTTPException, statusrouter = APIRouter()@router.post("/posts", response_model=PostDetailsModel, status_code=201)async def create_post(post: PostModel, current_user: User = Depends(get_current_user)):    post = await post_utils.create_post(post, current_user)    return post@router.get("/posts")async def get_posts(page: int = 1):    total_cout = await post_utils.get_posts_count()    posts = await post_utils.get_posts(page)    return {"total_count": total_cout, "results": posts}@router.get("/posts/{post_id}", response_model=PostDetailsModel)async def get_post(post_id: int):    return await post_utils.get_post(post_id)@router.put("/posts/{post_id}", response_model=PostDetailsModel)async def update_post(    post_id: int, post_data: PostModel, current_user=Depends(get_current_user)):    post = await post_utils.get_post(post_id)    if post["user_id"] != current_user["id"]:        raise HTTPException(            status_code=status.HTTP_403_FORBIDDEN,            detail="You don't have access to modify this post",        )    await post_utils.update_post(post_id=post_id, post=post_data)    return await post_utils.get_post(post_id)


Подключим новые роуты, добавив в main.py
from app.routers import postsapp.include_router(posts.router)


Тестирование


Тесты мы будем писать на pytest:
$ pip install pytest

Для тестирования эндпоинтов FastAPI предоставляет специальный инструмент TestClient.
Напишем тест для эндпоинта, который не требует подключения к базе данных:
from app.main import appfrom fastapi.testclient import TestClientclient = TestClient(app)def test_health_check():    response = client.get("/")    assert response.status_code == 200    assert response.json() == {"Hello": "World"}

Как видите, все достаточно просто. Необходимо инициализировать TestClient, и использовать его для тестирования HTTP запросов.

Для тестирования остальных эндпоинтов, необходимо создать тестовую БД. Отредактируем файл main.py, добавив в него конфигурацию тестовой базы:
from os import environimport databasesDB_USER = environ.get("DB_USER", "user")DB_PASSWORD = environ.get("DB_PASSWORD", "password")DB_HOST = environ.get("DB_HOST", "localhost")TESTING = environ.get("TESTING")if TESTING:    # Используем отдельную базу данных для тестов    DB_NAME = "async-blogs-temp-for-test"    TEST_SQLALCHEMY_DATABASE_URL = (        f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}"    )    database = databases.Database(TEST_SQLALCHEMY_DATABASE_URL)else:    DB_NAME = "async-blogs"    SQLALCHEMY_DATABASE_URL = (        f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:5432/{DB_NAME}"    )    database = databases.Database(SQLALCHEMY_DATABASE_URL)

Мы по-прежнему используем БД async-blogs для нашего приложения. Но если задано значение переменной окружение TESTING, тогда использовуется БД async-blogs-temp-for-test.

Чтобы база async-blogs-temp-for-test автоматически создавалась при запуске тестов и удалялась после их выполнения, создадим фикстуру в файле tests/conftest.py:
import osimport pytest# Устанавливаем `os.environ`, чтобы использовать тестовую БДos.environ['TESTING'] = 'True'from alembic import commandfrom alembic.config import Configfrom app.models import databasefrom sqlalchemy_utils import create_database, drop_database@pytest.fixture(scope="module")def temp_db():    create_database(database.TEST_SQLALCHEMY_DATABASE_URL) # Создаем БД    base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))    alembic_cfg = Config(os.path.join(base_dir, "alembic.ini")) # Загружаем конфигурацию alembic     command.upgrade(alembic_cfg, "head") # выполняем миграции    try:        yield database.TEST_SQLALCHEMY_DATABASE_URL    finally:        drop_database(database.TEST_SQLALCHEMY_DATABASE_URL) # удаляем БД

Для создания и удаления БД воспользуемся библиотекой sqlalchemy_utils .

Используя фикстуру temp_db в тестах, мы сможем протестировать все эндпоинты нашего приложения:
def test_sign_up(temp_db):    request_data = {        "email": "vader@deathstar.com",        "name": "Darth Vader",        "password": "rainbow"    }    with TestClient(app) as client:        response = client.post("/sign-up", json=request_data)    assert response.status_code == 200    assert response.json()["id"] == 1    assert response.json()["email"] == "vader@deathstar.com"    assert response.json()["name"] == "Darth"    assert response.json()["token"]["expires"] is not None    assert response.json()["token"]["access_token"] is not None

tests/test_posts.py
import asynciofrom app.main import appfrom app.schemas.users import UserCreatefrom app.utils.users import create_user, create_user_tokenfrom fastapi.testclient import TestClientdef test_create_post(temp_db):    user = UserCreate(        email="vader@deathstar.com",        name="Darth",        password="rainbow"    )    request_data = {      "title": "42",      "content": "Don't panic!"    }    with TestClient(app) as client:        # Create user and use his token to add new post        loop = asyncio.get_event_loop()        user_db = loop.run_until_complete(create_user(user))        response = client.post(            "/posts",            json=request_data,            headers={"Authorization": f"Bearer {user_db['token']['token']}"}        )    assert response.status_code == 201    assert response.json()["id"] == 1    assert response.json()["title"] == "42"    assert response.json()["content"] == "Don't panic!"def test_create_post_forbidden_without_token(temp_db):    request_data = {      "title": "42",      "content": "Don't panic!"    }    with TestClient(app) as client:        response = client.post("/posts", json=request_data)    assert response.status_code == 401def test_posts_list(temp_db):    with TestClient(app) as client:        response = client.get("/posts")    assert response.status_code == 200    assert response.json()["total_count"] == 1    assert response.json()["results"][0]["id"] == 1    assert response.json()["results"][0]["title"] == "42"    assert response.json()["results"][0]["content"] == "Don't panic!"def test_post_detail(temp_db):    post_id = 1    with TestClient(app) as client:        response = client.get(f"/posts/{post_id}")    assert response.status_code == 200    assert response.json()["id"] == 1    assert response.json()["title"] == "42"    assert response.json()["content"] == "Don't panic!"def test_update_post(temp_db):    post_id = 1    request_data = {      "title": "42",      "content": "Life? Don't talk to me about life."    }    with TestClient(app) as client:        # Create user token to add new post        loop = asyncio.get_event_loop()        token = loop.run_until_complete(create_user_token(user_id=1))        response = client.put(            f"/posts/{post_id}",            json=request_data,            headers={"Authorization": f"Bearer {token['token']}"}        )    assert response.status_code == 200    assert response.json()["id"] == 1    assert response.json()["title"] == "42"    assert response.json()["content"] == "Life? Don't talk to me about life."def test_update_post_forbidden_without_token(temp_db):    post_id = 1    request_data = {      "title": "42",      "content": "Life? Don't talk to me about life."    }    with TestClient(app) as client:        response = client.put(f"/posts/{post_id}", json=request_data)    assert response.status_code == 401


tests/test_users.py
import asyncioimport pytestfrom app.main import appfrom app.schemas.users import UserCreatefrom app.utils.users import create_user, create_user_tokenfrom fastapi.testclient import TestClientdef test_sign_up(temp_db):    request_data = {        "email": "vader@deathstar.com",        "name": "Darth",        "password": "rainbow"    }    with TestClient(app) as client:        response = client.post("/sign-up", json=request_data)    assert response.status_code == 200    assert response.json()["id"] == 1    assert response.json()["email"] == "vader@deathstar.com"    assert response.json()["name"] == "Darth"    assert response.json()["token"]["expires"] is not None    assert response.json()["token"]["token"] is not Nonedef test_login(temp_db):    request_data = {"username": "vader@deathstar.com", "password": "rainbow"}    with TestClient(app) as client:        response = client.post("/auth", data=request_data)    assert response.status_code == 200    assert response.json()["token_type"] == "bearer"    assert response.json()["expires"] is not None    assert response.json()["access_token"] is not Nonedef test_login_with_invalid_password(temp_db):    request_data = {"username": "vader@deathstar.com", "password": "unicorn"}    with TestClient(app) as client:        response = client.post("/auth", data=request_data)    assert response.status_code == 400    assert response.json()["detail"] == "Incorrect email or password"def test_user_detail(temp_db):    with TestClient(app) as client:        # Create user token to see user info        loop = asyncio.get_event_loop()        token = loop.run_until_complete(create_user_token(user_id=1))        response = client.get(            "/users/me",            headers={"Authorization": f"Bearer {token['token']}"}        )    assert response.status_code == 200    assert response.json()["id"] == 1    assert response.json()["email"] == "vader@deathstar.com"    assert response.json()["name"] == "Darth"def test_user_detail_forbidden_without_token(temp_db):    with TestClient(app) as client:        response = client.get("/users/me")    assert response.status_code == 401@pytest.mark.freeze_time("2015-10-21")def test_user_detail_forbidden_with_expired_token(temp_db, freezer):    user = UserCreate(        email="sidious@deathstar.com",        name="Palpatine",        password="unicorn"    )    with TestClient(app) as client:        # Create user and use expired token        loop = asyncio.get_event_loop()        user_db = loop.run_until_complete(create_user(user))        freezer.move_to("'2015-11-10'")        response = client.get(            "/users/me",            headers={"Authorization": f"Bearer {user_db['token']['token']}"}        )    assert response.status_code == 401



P.S. Исходники


Вот собственно и все, репозиторий с исходниками из поста можно посмотреть на GitHub.
Подробнее..

FastAPI Dependency Injector

18.11.2020 10:21:27 | Автор: admin


Привет,

Я выпустил новую версию Dependency Injector 4.4. Она позволяет использовать Dependency Injector вместе с FastAPI. В этом посте покажу как это работает.

Основная задача интеграции: подружить директиву Depends FastAPI c маркерами Provide и Provider Dependency Injector'a.

Из коробки до версии DI 4.4 это не работало. FastAPI использует типизацию и Pydantic для валидации входных параметров и ответа. Маркеры Dependency Injector'а приводили его в недоумение.

Решение пришло после изучения внутренностей FastAPI. Пришлось сделать нескольких изменений в модуле связывания (wiring) Dependency Injector'а. Директива Depends теперь работает вместе с маркерами Provide и Provider.

Пример


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

import sysfrom fastapi import FastAPI, Dependsfrom dependency_injector import containers, providersfrom dependency_injector.wiring import inject, Provideclass Service:    async def process(self) -> str:        return 'Ok'class Container(containers.DeclarativeContainer):    service = providers.Factory(Service)app = FastAPI()@app.api_route('/')@injectasync def index(service: Service = Depends(Provide[Container.service])):    result = await service.process()    return {'result': result}container = Container()container.wire(modules=[sys.modules[__name__]])

Для того чтобы запустить пример установите зависимости:

pip install fastapi dependency-injector uvicorn

и запустите uvicorn:

uvicorn fastapi_di_example:app --reload

В терминале должно будет появится что-то вроде:

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)INFO:     Started reloader process [11910] using watchgodINFO:     Started server process [11912]INFO:     Waiting for application startup.INFO:     Application startup complete.

а http://127.0.0.1:8000 должен возвращать:

{    "result": "Ok"}


Как тестировать?


Создайте рядом файл tests.py и поместите в него следующие строки:

from unittest import mockimport pytestfrom httpx import AsyncClientfrom fastapi_di_example import app, container, Service@pytest.fixturedef client(event_loop):    client = AsyncClient(app=app, base_url='http://test')    yield client    event_loop.run_until_complete(client.aclose())@pytest.mark.asyncioasync def test_index(client):    service_mock = mock.AsyncMock(spec=Service)    service_mock.process.return_value = 'Foo'    with container.service.override(service_mock):        response = await client.get('/')    assert response.status_code == 200    assert response.json() == {'result': 'Foo'}

Для того чтобы запустить тесты установите зависимости:

pip install pytest pytest-asyncio httpx

и запустите pytest:

pytest tests.py

В терминале должно будет появится:

======= test session starts =======platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1rootdir: ...plugins: asyncio-0.14.0collected 1 itemtests.py .                                                      [100%]======= 1 passed in 0.17s =======

Что дает интеграция?


FastAPI классный фреймворк для построения API. В него встроен базовый механизм dependency injection.

Эта интеграция улучшает работу dependency injection в FastAPI. Она позволяет использовать в нём провайдеры, переопредение, конфиг и ресурсы Dependency Injector'а.

Что дальше?


Подробнее..
Категории: Python , Dependency injection , Fastapi

Локальный видеохостинг. Часть 0. Определяемся с правилами

15.06.2021 12:07:46 | Автор: admin

Предыстория

Все мы любим кино, сериалы и прочие видосики в интернете. После не долгих размышлений, пришла идея написать небольшой и очень простой сервер с возможностью просмотра видео на разных устройствах.

Основные фичи

  • Просмотр на разных устройствах

  • Автоматическое обновление коллекции путем сканирования директорий

  • Возможность продолжить просмотр с того же места, где остановился

  • Возможность добавления новых видео в коллекцию

  • Сделать максимально легкий сервис, чтобы была возможность запускать даже на слабом Raspberry Pi

  • Отказ от лишних сервисов/зависимостей в угоду экономии оперативной памяти

  • Максимально поддерживаемое количество форматов, без перекодировки и сегментирования

Стек

В качестве первого решения я хочу взять Python и FastApi для Backend, сложно сказать, что будет являться основным решением Frontend, но точно это будет поддержка внешних видеоплееров, например, VLC. В дальнейшем возможно будет добавлен и встроенный плеер в веб страницу, но предчувствую проблемы с кодеками и прочее. Более детально рассмотрю Frontend решения в следующих частях.

Итог

С задачей примерно определились, в процессе думаю, что она будет усложняться и обрастать новыми фичами. Что касается аналогов, то я прекрасно знаю как минимум о Kodi для того же Raspberry Pi, и все это похоже на создание велосипеда, но это всего лишь идея которую возможно кто-то подхватит в качестве пет проекта или студенту ИТ специальности нужен будет проект для курсовой работы :)

Своими идеи связанные с библиотеками/фичами/тулзами и прочим оставляйте к комментариях, в следующей части постараюсь расширить список фич и выкатить Backend с описанием.

Подробнее..
Категории: Python , Видео , Fastapi

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru