Скидки до 50% и курс по ИИ в подарок 2 дня 12 :27 :27 Выбрать курс
Код
#статьи

Знакомимся с FastAPI и пишем собственный API

Расскажем про быстрый самодокументирующийся фреймворк для создания веб-API.

Иллюстрация: Оля Ежак для Skillbox Media

Python — не самый быстрый язык, и для задач с высокими нагрузками разработчики часто выбирают Go или Node.js. Но с появлением FastAPI ситуация изменилась: теперь на Python можно создавать быстрые, асинхронные и легко масштабируемые веб-приложения. В этой статье рассказываем, что делает FastAPI особенным, и показываем, как с нуля собрать рабочий API с автоматической документацией.

Содержание


Что такое FastAPI

FastAPI — это современный фреймворк для создания веб-приложений и API на Python, который стал популярным благодаря простоте и скорости работы. С его помощью можно буквально за несколько строк кода создать сервер, который принимает запросы, обрабатывает их и возвращает результаты — например, данные из базы, ответ нейросети или результат вычислений.

FastAPI создали в 2018 году, чтобы исправить слабые места старых Python-фреймворков вроде Flask и Django. Эти инструменты отлично подходили для создания классических сайтов, но начинали тормозить с ростом числа запросов и переходом на асинхронный Python.

Новый фреймворк собрал под капотом несколько важных технологий, которые делают его быстрым и удобным:

  • Starlette отвечает за ядро приложения — то, как сервер принимает и обрабатывает запросы. Благодаря ей FastAPI работает асинхронно и выдерживает большую нагрузку.
  • Pydantic помогает проверять данные — например, если пользователь отправил текст вместо числа, фреймворк сразу покажет ошибку, не давая «упасть» всему приложению.
  • Аннотации типов Python позволяют писать код понятнее и безопаснее — вы просто указываете, каких данных ждёте, а FastAPI сам проверяет их и подставляет всё нужное в документацию.

FastAPI применяют там, где нужно оперативно обрабатывать запросы, — от небольших сервисов до крупных платформ. Python-разработчики создают на нём бэкенд веб-приложений, DevOps-инженеры и инженеры по интеграции используют его для микросервисов и соединения разных систем между собой, а ML-специалисты разворачивают с его помощью модели и делают их доступными через API.

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

Чем хорош FastAPI

Главные достоинства FastAPI — высокая производительность, надёжность и автоматическая документация.

Производительность. Сравнима с Node.js и Go благодаря асинхронному подходу: фреймворк не ждёт, пока одна операция полностью завершится, прежде чем начать следующую. Он может обрабатывать множество запросов одновременно — или, точнее, переключаться между ними так быстро, что это почти не ощущается.

Производительность фреймворка подтвердили независимые тесты, в частности тест Sharkbench: приложения FastAPI показали одну из самых высоких скоростей среди Python-фреймворков. Это значительно экономит ресурсы сервера, что подходит для приложений с большим трафиком.

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

Например, вот простая функция, которая складывает два числа без аннотаций:

def add(a, b):
    return a + b

result = add(5, 3)  # работает
result2 = add("5", "3")  # тоже работает, но результат будет "53", что может быть ошибкой

В этом примере Python не знает, какие типы должны быть у a и b. Поэтому функция складывает числа или объединяет строки в зависимости от того, какие данные передали. Если типы неверны, ошибки не будет, но результат получится неправильным.

А вот пример FastAPI с типизацией прямо в параметрах функции:

from fastapi import FastAPI

app = FastAPI()

# Параметры a и b сразу типизируем как int
@app.get("/add")
def add_numbers(a: int, b: int):
    return {"result": a + b}

Здесь a: int и b: int — FastAPI сразу проверяет, что в запросе пришли числа. Сам запрос выглядит так:

GET /add?a=5&b=3

Ответ:

{
  "result": 8
}

Если передать не число, например a=five, FastAPI автоматически вернёт ошибку и скажет, что данные некорректны.

Интерактивная документация. FastAPI генерирует документацию автоматически. Он сам создаёт страницу, где видно все доступные эндпойнты (адреса запросов), параметры и возможные ответы.

Когда вы запускаете приложение, FastAPI добавляет два специальных маршрута — обычно это /docs и /redoc. Если открыть их в браузере, вы увидите интерактивную страницу: можно выбрать любой метод API, посмотреть, какие данные он принимает, и протестировать прямо из браузера.

Разработчики, тестировщики и аналитики могут видеть, как меняется структура API после обновлений кода, отслеживать новые поля, искать ошибки, проверять ответы сервера.

Недостатки FastAPI

FastAPI — классный и быстрый фреймворк, но у него есть свои минусы.

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

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

Недостаточно развитая экосистема. Если Django — это полновесный фреймворк для веб-разработки, то FastAPI напоминает, скорее, набор деталей, из которых нужно собрать всё самому. Здесь нет готовой админ-панели, систем для фоновых задач или модулей управления пользователями — алгоритмы регистрации, авторизации и подтверждения почты приходится писать вручную.

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

Создаём свой API с помощью FastAPI

Создадим полноценный API — с поддержкой баз данных и сервисом авторизации и аутентификации. Не беспокойтесь, если вы раньше не работали с базами данных: мы будем их имитировать. Но знать базовый Python версии 3.8 или новее и основы работы с терминалом нужно, иначе разобраться будет сложно.

Настраиваем проект

Сначала создадим виртуальное окружение. Виртуальное окружение — это отдельная среда для Python, огороженная от общей среды в вашей ОС. Её создают, чтобы пакеты не конфликтовали с системой. Выполните в терминале:

python -m venv fastapi_project

Или:

python3 -m venv fastapi_project

Вы создали папку fastapi_project и окружение. Если всё сделано правильно, то внутри папки fastapi_project будут папки bin, include, lib, lib64 и файл pyvenv.cfg.

Теперь окружение нужно активировать. На Linux или Mac:

source fastapi_project/bin/activate

На Windows:

fastapi_project\Scripts\activate

Если вы следовали инструкции, то вы увидите (fastapi_project) перед строкой приглашения в терминале. Это значит, что окружение активно.

Скриншот: xfce4-terminal / Skillbox Media

Установите FastAPI:

pip install fastapi[standard]

Теперь проект настроен, можно начинать работу.

Hello, World!

Создадим первый API. Создайте внутри папки fastapi_project файл main.py и добавьте следующий код:

from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
    return {"message": "Hello World"}

Запустите сервер, находясь в папке fastapi_project:

fastapi dev main.py

Откройте браузер и перейдите по ссылке http://127.0.0.1:8000. Вы увидите {"message": "Hello World"}.

Скриншот: Google Chrome / Skillbox Media

Документация доступна по ссылке http://127.0.0.1:8000/docs. Нажмите на GET /, Try it out, затем Execute — и увидите ответ.

Скриншот: FastAPI / Skillbox Media

В терминале ответ тоже отобразится.

Скриншот: xfce4-terminal / Skillbox Media

Чтобы остановить сервер, нажмите Ctrl + C. Перезапускать сервер после изменений в коде не нужно: FastAPI умеет автоматически подгружать изменения. Если в вашем редакторе кода нет автосохранения, не забывайте сохранять файл после каждого изменения, иначе код на сервере не поменяется.

Прописываем эндпойнты и параметры

Эндпойнты — это адреса в вашем API, по которым клиенты отправляют запросы. Мы уже обращались к автоматически прописанному эндпойнту /docs, когда открывали документацию. Теперь пропишем свои. Создайте файл endpoints.py внутри fastapi_project:

from fastapi import FastAPI
# Функция для регистрации эндпойнтов
def register_endpoints(app: FastAPI):
    # Эндпойнт для главной страницы
    @app.get("/")
    async def root():
        return {"message": "Hello World"}

    # Эндпойнт с path-параметром item_id (часть URL)
    # Например, /items/5 вернёт {"item_id": 5}
    @app.get("/items/{item_id}")
    async def read_item(item_id: int):
        # item_id: int -- параметр должен быть числом
        return {"item_id": item_id}

Отредактируем main.py — импортируем функцию register_endpoints и вызовем её:

from fastapi import FastAPI
from endpoints import register_endpoints

# Создаём приложение FastAPI
app = FastAPI()

# Регистрируем эндпойнты
register_endpoints(app)

Убедимся, что всё работает верно. Перейдите по http://127.0.0.1:8000/items/5 — получите {"item_id": 5}.

Скриншот: Google Chrome / Skillbox Media

Если ввести http://127.0.0.1:8000/items/abc, FastAPI выдаст ошибку, так как ожидает число.

Скриншот: Google Chrome / Skillbox Media

Конструкцию вида {"item_id": 5} называют path-параметром — это часть URL, которая может меняться. В нашем случае после items/ можно указать любое целое число.

Добавим query-параметр — это данные, которые передаются в URL после ?. Например, http://127.0.0.1:8000/items/5?q=hello передаёт параметр q со значением hello. С помощью query-параметров удобно передавать дополнительные данные, например поисковые запросы. Отредактируем эндпойнт items/:

# Эндпойнт с path- и query-параметрами
    # Например, /items/5?q=hello вернёт {"item_id": 5, "q": "hello"}
    @app.get("/items/{item_id}")
    async def read_item(item_id: int, q: str = None):
        # q: str = None -- query-параметр, строка или ничего
        return {"item_id": item_id, "q": q}

Теперь по адресу http://127.0.0.1:8000/items/5?q=hello мы увидим ответ {"item_id": 5, "q": "hello"}.

Скриншот: Google Chrome / Skillbox Media

А по адресу http://127.0.0.1:8000/items/5 — {"item_id": 5, "q": null}.

Скриншот: Google Chrome / Skillbox Media

Иногда нужно ограничить значения, которые пользователь может передать в URL. Например, мы хотим прописать эндпойнт models/ с параметром {model}, который будет принимать только два значения: a и b. Для этого используем встроенный модуль Enum — он отлично подходит для перечисления констант. Добавьте импорт Enum и класс для хранения значений в endpoints.py:

from enum import Enum
# Определяем Enum — список разрешённых значений
class Model(str, Enum):
    a = "a"  # Разрешённое значение "a"
    b = "b"  # Разрешённое значение "b"

Добавим в конец функции register_endpoints новый эндпойнт (следите за отступами):

# Эндпойнт /models/{model} принимает только a или b
    @app.get("/models/{model}")
    async def get_model(model: Model):
        # model: Model -- проверяет, что значение из Enum
        return {"model": model}

Перейдите на http://127.0.0.1:8000/models/a — увидите {"model": "a"}. Аналогично для http://127.0.0.1:8000/models/b. Если перейти по http://127.0.0.1:8000/models/c, FastAPI выдаст ошибку, так как c не входит в список разрешённых значений, определённых в Model.

Скриншот: Google Chrome / Skillbox Media

Добавим два эндпойнта для пользователей — один фиксированный и один динамический:

# Фиксированный путь для текущего пользователя
    @app.get("/users/me")
    async def read_user_me():
        return {"user_id": "me"}

    # Динамический путь для любого пользователя
    @app.get("/users/{user_id}")
    async def read_user(user_id: str):
        # user_id: str -- принимает строку
        return {"user_id": user_id}

Порядок важен, чтобы FastAPI не путал их. Теперь по адресу http://127.0.0.1:8000/users/me будет ответ {"user_id": "me"}.

По адресу, например, http://127.0.0.1:8000/users/igor будет ответ {"user_id": "igor"}.

Обрабатываем и проверяем запросы

Добавим возможность принимать данные от пользователя через тело запроса. Тело запроса выступает в качестве формы, которую пользователь заполняет и отправляет на сервер.

Мы будем использовать Pydantic — инструмент, который проверяет, имеют ли данные правильный формат (например, число — это число, а не буквы). Если данные неверные, FastAPI автоматически вернёт ошибку. Также мы добавим проверку, чтобы отклонять слишком дорогие товары.

Обновите импорты endpoints.py:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from enum import Enum

Добавьте класс для описания структуры данных:

class Item(BaseModel):
    name: str  # Название — строка
    price: float  # Цена — число
    description: str | None = None  # Описание — строка или ничего

Добавьте новый эндпойнт в конец register_endpoints:

# Эндпойнт для создания товара через POST
    @app.post("/items/")
    async def create_item(item: Item):
        # Проверяем цену
        if item.price > 100:
            raise HTTPException(status_code=400, detail="Too expensive")
        return item

Откройте http://127.0.0.1:8000/docs, найдите эндпойнт POST /items/, нажмите Try it out и отправьте запрос в формате JSON. Это формат данных, похожий на словарь Python:

{
  "name": "apple",
  "price": 1.5,
  "description": "A juicy apple"
}

Вы получите ответ {"name": "apple", "price": 1.5, "description": "A juicy apple"}. Если отправить {"name": "apple", "price": "abc"}, FastAPI вернёт ошибку 422, так как price должен быть числом. Если отправить {"name": "apple", "price": 150}, вы получите ошибку 400 с сообщением «Too expensive», потому что цена выше 100.

Пишем сервис аутентификации и авторизации

Аутентификация проверяет, кто вы, а авторизация — что вы можете делать. Мы добавим два способа защиты: API-ключ (секретный код в заголовке запроса) и HTTP Basic (имя пользователя и пароль). Начнём с ключа. Создайте файл auth.py в fastapi_project:

from fastapi import Depends, HTTPException
from fastapi.security import APIKeyHeader

# Настраиваем API-ключ в заголовке X-Key
api_key = APIKeyHeader(name="X-Key")
fake_keys = ["secret"]

# Функция проверки API-ключа
def get_key(key: str = Depends(api_key)):
    if key not in fake_keys:
        raise HTTPException(status_code=401, detail="Invalid API Key")
    return key

В endpoints.py обновите импорты:

from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from enum import Enum
from auth import get_key

Замените эндпойнт get.items/ на эндпойнт с API-ключем в endpoints.py:

 # Эндпойнт с API-ключом
    @app.get("/items/{item_id}")
    async def read_item(item_id: int, q: str = None, key=Depends(get_key)):
        return {"item_id": item_id, "q": q}

В /docs для GET /items/{item_id} введите secret в Authorize. Форма для входа доступна по появляющейся справа зелёной кнопке с иконкой замка.

Скриншот: FastAPI / Skillbox Media

Проверьте с item_id=5 — получите ответ {"item_id": 5, "q": null}. С неверным ключом вылезет ошибка 401.

Ответ сервера при попытке запроса с неправильным API-ключом
Скриншот: FastAPI / Skillbox Media

Добавим авторизацию. Обновите импорты в auth.py:

from fastapi import Depends, HTTPException
from fastapi.security import APIKeyHeader, HTTPBasic, HTTPBasicCredentials
from secrets import compare_digest

Добавьте в конец файла проверку имени и пароля для обычного пользователя и для админа:

# HTTP Basic
basic = HTTPBasic()

# Функция проверки имени и пароля
def get_user(credentials: HTTPBasicCredentials = Depends(basic)):
        if not compare_digest(credentials.password, "pass"):
        raise HTTPException(status_code=401, detail="Invalid credentials")
    return credentials.username

# Проверка админа
def admin_only(user: str = Depends(get_user)):
    if user != "admin":
        raise HTTPException(status_code=403, detail="Admins only")
    return user

В endpoints.py обновляем импорт из auth.py:

from auth import get_key, get_user, admin_only

Обновляем эндпойнт с фиксированным пользователем:

# Эндпойнт с HTTP Basic
    @app.get("/users/me")
    async def read_user_me(user: str = Depends(get_user)):
        return {"user_id": user}

Добавляем админский эндпойнт:

# Эндпойнт только для админа
    @app.get("/admin/")
    async def admin_only_endpoint(user: str = Depends(admin_only)):
        return {"message": "Welcome, admin"}

Убедимся, что всё работает. Зайдите в /docs. Для GET /users/me введите логин user и пароль pass в Authorize → HTTP Basic. В ответе должно быть {"user_id": <ваш логин>}.

Скриншот: FastAPI / Skillbox Media

Для GET /admin/ попробуйте Try it out → Execute без авторизации — появится ошибка 403 с сообщением «Admin’s only».

Скриншот: FastAPI / Skillbox Media

Если ввести логин admin и пароль pass, появится ответ {"message": "Welcome, admin"}.

Скриншот: FastAPI / Skillbox Media

Имитируем базу данных и подключаем к проекту

Создадим database.py в fastapi_project:

import json

# Функция загрузки данных из db.json
def load_db():
    try:
        with open("db.json", "r") as f:
            return json.load(f)  # Читаем JSON как список
    except FileNotFoundError:
        # Если файла нет, возвращаем пустой список
        return []

# Функция сохранения данных в db.json
def save_db(data):
    with open("db.json", "w") as f:
        json.dump(data, f)  # Записываем список в JSON

# Загружаем базу данных при старте
db = load_db()

Добавим импорт в endpoints.py:

from database import db, save_db

В register_endpoints замените старый эндпойнт post/items/ на следующий:

# Эндпойнт для создания товара. Сохраняет товар в db.json
    @app.post("/items/")
    async def create_item(item: Item):
        # Проверяем цену
        if item.price > 100:
            raise HTTPException(status_code=400, detail="Too expensive")
        db.append(item.dict())  # Добавляем товар в список
        save_db(db)  # Сохраняем в db.json
        return item

В /docs отправьте POST-запрос на /items/:

{
  "name": "apple",
  "price": 1.5,
  "description": "A juicy apple"
}
Скриншот: FastAPI / Skillbox Media

Товар сохранится в автоматически созданном файле db.json.

Чтобы посмотреть все добавленные товары, пропишем ещё один эндпойнт в endpoints.py:

# Эндпойнт для получения всех товаров
    @app.get("/items/")
    async def get_items():
        return db

В /docs выполните GET-запрос на /items/ — увидите список товаров, например [{"name": "apple", "price": 1.5, "description": "A juicy apple"}].

Скриншот: FastAPI / Skillbox Media

Обзор проекта

Наш проект готов! В итоге у вас в папке fastapi_project должно быть пять файлов: main.py, endpoints.py, auth.py, database.py и db.json. Если что-то работает не так, как задумано, сверьте код.

main.py:

from fastapi import FastAPI
from endpoints import register_endpoints

# Создаём приложение FastAPI
app = FastAPI()

# Регистрируем эндпойнты
register_endpoints(app)

endpoints.py:

from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from enum import Enum
from auth import get_key, get_user, admin_only
from database import db, save_db

class Item(BaseModel):
    name: str  # Название — строка
    price: float  # Цена — число
    description: str | None = None  # Описание — строка или ничего


# Определяем Enum — список разрешённых значений
class Model(str, Enum):
    a = "a"  # Разрешённое значение "a"
    b = "b"  # Разрешённое значение "b"


# Функция для регистрации эндпойнтов
def register_endpoints(app: FastAPI):
    # Эндпойнт для главной страницы
    @app.get("/")
    async def root():
        return {"message": "Hello World"}

# Эндпойнт с API-ключом
    @app.get("/items/{item_id}")
    async def read_item(item_id: int, q: str = None, key=Depends(get_key)):
        return {"item_id": item_id, "q": q}

    # Эндпойнт /models/{model} принимает только a или b
    @app.get("/models/{model}")
    async def get_model(model: Model):
        # model: Model — проверяет, что значение из Enum
        return {"model": model}

    # Фиксированный путь для текущего пользователя
    @app.get("/users/me")
    async def read_user_me(user: str = Depends(get_user)):
        return {"user_id": user}


    # Динамический путь для любого пользователя
    @app.get("/users/{user_id}")
    async def read_user(user_id: str):
        # user_id: str — принимает строку
        return {"user_id": user_id}


    # Эндпойнт только для админа
    @app.get("/admin/")
    async def admin_only_endpoint(user: str = Depends(admin_only)):
        return {"message": "Welcome, admin"}

    # Эндпойнт для создания товара. Сохраняет товар в db.json
    @app.post("/items/")
    async def create_item(item: Item):
        # Проверяем цену
        if item.price > 100:
            raise HTTPException(status_code=400, detail="Too expensive")
        db.append(item.dict())  # Добавляем товар в список
        save_db(db)  # Сохраняем в db.json
        return item

    # Эндпойнт для получения всех товаров
    @app.get("/items/")
    async def get_items():
        return db

auth.py:

from fastapi import Depends, HTTPException
from fastapi.security import APIKeyHeader, HTTPBasic, HTTPBasicCredentials
from secrets import compare_digest

# Настраиваем API-ключ
api_key = APIKeyHeader(name="X-Key")
fake_keys = ["secret"]

def get_key(key: str = Depends(api_key)):
    if key not in fake_keys:
        raise HTTPException(status_code=401, detail="Invalid API Key")
    return key

# Настраиваем HTTP Basic
basic = HTTPBasic()

# Функция проверки имени и пароля
def get_user(credentials: HTTPBasicCredentials = Depends(basic)):
    # Проверяем только пароль «pass», имя может быть любым
    if not compare_digest(credentials.password, "pass"):
        raise HTTPException(status_code=401, detail="Invalid credentials")
    return credentials.username

# Проверка админа
def admin_only(user: str = Depends(get_user)):
    if user != "admin":
        raise HTTPException(status_code=403, detail="Admins only")
    return user

database.py:

import json

# Функция загрузки данных из db.json
def load_db():
    try:
        with open("db.json", "r") as f:
            return json.load(f)  # Читаем JSON как список
    except FileNotFoundError:
        # Если файла нет, возвращаем пустой список
        return []

# Функция сохранения данных в db.json
def save_db(data):
    with open("db.json", "w") as f:
        json.dump(data, f)  # Записываем список в JSON

# Загружаем базу данных при старте
db = load_db()

db.json будет хранить всё, что вы сохранили с помощью POST-запросов. В нашем случае там будет один товар:

{
  "name": "apple",
  "price": 1.5,
  "description": "A juicy apple"
}

Убедитесь, что финальные версии файлов сохранены, а команды в терминале написаны без ошибок.

Что дальше?

Чтобы глубже понять FastAPI, изучите официальную документацию. Она охватывает все фичи: от асинхронности до интеграций. Там есть туториалы, примеры кода и советы по best practices. Документация на английском, но с автоматическим переводом на русский. Начните с раздела «Tutorial — User Guide». Она обновляется регулярно, так что информация всегда свежая. Это лучший источник для освоения продвинутых тем.

Для проекта мы имитировали базы данных. Но для реальных проектов это не годится. Чтобы понять, как правильно, изучите реляционные и нереляционные базы данных.

Реальные проекты нужно тестировать. На Python есть несколько библиотек для тестирования. Самая популярная — Pytest.

Чтобы проект был доступен другим пользователям, его нужно грамотно развернуть. Сегодня стандарт индустрии — Docker. Тому, как развернуть с его помощью приложение на FastAPI, посвящён отдельный подраздел в документации.

Больше интересного про код — в нашем телеграм-канале.  Подписывайтесь!



Python для всех

Вы освоите Python на практике и создадите проекты для портфолио — телеграм-бот, веб-парсер и сайт с нуля. А ещё получите готовый план выхода на удалёнку и фриланс. Спикер — руководитель отдела разработки в «Сбере».

Пройти бесплатно



Учитесь Python бесплатно ➞

Пройдите бесплатный курс по разработке на Python. Получите готовый план выхода на фриланс и полезный гайд по созданию резюме.

Пройти курс
Бесплатный курс по разработке на Python ➞
Пройдите бесплатный курс по Python и создайте с нуля телеграм-бот, веб-парсер и сайт. Спикер — руководитель отдела разработки в «Сбере».
Пройти курс
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована