Целью данной лабораторной работы является создание веб-приложения для работы с абстрактными элементами (items). Для достижения этой цели мы будем использовать язык программирования Python, веб-фреймворк FastAPI и контейнеризацию с помощью Docker. Работа включает в себя установку необходимых инструментов, написание кода приложения, и контейнеризацию для обеспечения удобства развертывания.

Теоретическая база

Python и FastAPI

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

Docker

Docker используется для контейнеризации приложения, что позволяет упаковать все зависимости и окружение в один контейнер. Это обеспечивает переносимость и упрощает развертывание приложения.

Цели и задачи

  1. Установка Python и Docker.

  2. Создание виртуального окружения и установка зависимостей.

  3. Написание кода веб-приложения с использованием FastAPI.

  4. Тестирование приложения.

  5. Создание Docker-образа и запуск контейнера.

  6. Проверка работоспособности приложения в контейнере.

Технические требования

  • Python

  • Docker

  • pip-tools для управления зависимостями

  • FastAPI для разработки веб-приложения

Ожидаемые результаты

  1. Виртуальное окружение с установленными зависимостями.

  2. Работающее веб-приложение с возможностью добавления, изменения, удаления и получения элементов.

  3. Docker-образ, готовый для развертывания приложения.

  4. Проверка работоспособности приложения в контейнере.

Выполнение лабораторной работы

Настройка виртуального окружения и зависимостей:

  1. Склонируйте данный репозиторий

  2. Перейдите в него

  3. Создайте виртуальное окружение

    python3 -m venv venv

  4. Активируйте его

    source venv/bin/activate

  5. Установите pip-tools

    pip install pip-tools

  6. Создайте файл requirements.in и откройте его

  7. Запишите в данный файл следующие строки

    fastapi

    uvicorn

  8. Далее в консоли с активированным виртуальным окружением пишем

    pip-compile

  9. У нас автоматически создаться requirements.txt, в котором будут библиотеки и фреймворки совместимых версий. Далее данной командой все библиотеки установятся в наше виртуальное окружение.

    pip-sync

Написание веб-приложения:

Мы разработаем простое веб-приложение, которое будет работать с абстрактными элементами (назовем их items). Наше приложение сможет хранить items, также по запросу пользователя приложение сможет добавлять новые items, изменять их и удалять. В item будут лежать данные о его названии, цене и наличии.

1. Создать файл main.py и запустить его

2. Далее импортируем классы FastAPI, HTTPException, status для дальнейшей работы с ними

from fastapi import FastAPI, HTTPException, status

3. Создаем экземпляр класса FastAPI для создания нашего приложения. Также
нам потребуется создать список items для хранения данных и переменную
items_id для идентификации items

app = FastAPI()
items = []
items_id = 1

4. Создадим первый endpoint, который будет возвращать сообщение "Hello World"

@app.get('/')
def root():
    return {'message': 'Hello World'}

5. Далее создадим endpoint '/items', который будет обрабатывать GET-запросы и возвращать требуемый item, если он существует

@app.get('/items')
def get_item(item_id: int):
    try:
        return items[item_id - 1]
    except IndexError:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='item does not exists')

Проверка существования item происходит за счёт блока try except. Если элемента с переданным id не существует, будет вызвано исключение IndexError, так как мы выйдем за границы нашего списка. Если элемента не существует, мы оповещаем об этом пользователя статус-кодом 400 и сообщением "item does not exists"

6. Теперь создадим endpoint '/items', который будет обрабатывать POST-запросы, создавать item и добавлять его в список items

@app.post('/items')
def create_item(name: str, coast: float, in_stock: bool):
    global items_id
    item = {
        'id': items_id,
        'name': name,
        'coast': coast,
        'in_stock': in_stock
    }
    items.append(item)
    items_id += 1
    return item

Мы работаем с глобальной переменной items_id, так как у каждого item должен быть уникальный id, поэтому перед return увеличиваем её на 1

7. По этому же endpoint будем обрабатывать PATCH-запросы для частичного изменения требуемого item

@app.patch('/items')
def patch_item(item_id: int, new_name: str = None, new_coast: float = None, new_in_stock: bool = None):
    try:
        if new_name:
            items[item_id - 1]['name'] = new_name
            return items[item_id - 1]
        elif new_coast:
            items[item_id - 1]['coast'] = new_coast
            return items[item_id - 1]
        elif new_in_stock:
            items[item_id - 1]['in_stock'] = new_in_stock
            return items[item_id - 1]
    except IndexError:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='item does not exists')

Так же, как и при обработке GET-запросов будем проверять, существует ли требуемый item

8. Теперь будем обрабатывать PUT-запросы для полного изменения требуемого item

@app.put('/items')
def patch_item(item_id: int, new_name: str, new_coast: float, new_in_stock: bool):
    try:
        items[item_id - 1]['name'] = new_name
        items[item_id - 1]['coast'] = new_coast
        items[item_id - 1]['in_stock'] = new_in_stock
        return items[item_id - 1]
    except IndexError:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='item does not exists')

Все так же, как и в прошлом пункте, только требуемый item изменяется полностью

9. Создадим обработчик DELETE-запросов для удаления требуемого item

@app.delete('/items')
def delete_item(item_id: int):
    global items_id
    try:
        if item_id != len(items):
            for index in range(item_id, len(items)):
                items[index]['id'] -= 1
        item = items.pop(item_id - 1)
        items_id -= 1
        return item
    except IndexError:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='item does not exists')

Наше приложение готово. Теперь можно запустить его и проверить работоспособность.

Запуск веб-приложения:

Теперь мы можем запустить наше веб-приложение, для этого введем команду

uvicorn main:app

Теперь мы можем перейти по ссылке localhost:8000/docs и делать запросы к нашему приложению

Контейнеризация приложения:

1. Создать Dockerfile и запустить его

2. Сначала импортируем образ python

FROM python:yourversion

Вместо yourversion напишите вашу версию python

3. Теперь сделаем рабочую директорию app

WORKDIR /app

4. Далее скопируем файлы, необходимые для работы нашего приложения

COPY ./main.py /app
COPY ./requirements.txt /app

5. Установим зависимости внутри образа

RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

6. Наконец, напишем последнюю команду, которая будет исполняться при запуске контейнера

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]

Флаг --host 0.0.0.0 отвечает за то, чтобы наше веб-приложение работало на всех сетевых интерфейсах контейнера на порту 80.

7. Теперь создадим образ. Впишем в консоли

docker build -t fastapiapp .

8. Запустим контейнер, используя образ fastapiapp

docker run -d --name fastapiapp -p 8000:80 fastapiapp

Флаг -d отвечает за запуск контейнера в фоновом режиме. Флагом --name задаем имя нашему контейнеру. Флагом -p мы пробрасываем 80 порт контейнера на 8000 порт хоста

Проверка работоспособности:

Теперь можем перейти по ссылке localhost:8000/docs и проверить работоспособность нашего веб-приложения

Чтобы остановить работу контейнера, надо прописать данную команду в консоль

docker stop fastapiapp

Выводы

Лабораторная работа предоставила ценный опыт в области разработки backend-приложений, особенно в использовании веб-фреймворка FastAPI. Этот опыт оказывается крайне полезным для студентов и профессионалов, стремящихся погрузиться в мир веб-разработки и создания надежных серверных приложений.

Кроме того, работа с Docker в контексте данного проекта предоставила понимание принципов контейнеризации и умения упаковывать приложения со всеми их зависимостями в единый контейнер. Этот подход обеспечивает переносимость и удобство развертывания приложений в различных средах.

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

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

Список использованных источников:

  1. Документация FastAPI

  2. Документация Docker

  3. Примеры кода и руководства по созданию веб-приложений с FastAPI.

  4. Официальные ресурсы Python и pip-tools

Комментарии (12)


  1. chemdev
    15.11.2023 11:54

    Кто-то пользуется докер отдельно от docker-compose? Ведь как минимум всегда нужно прописать порты, зачем это каждый раз делать в терминале.


  1. icya
    15.11.2023 11:54

    Странно, в документации FastAPI есть и туториал, и пример использования (и оно даже на русский язык переведено), а у вас всё равно получилось что-то иное.

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


    1. Moonlization
      15.11.2023 11:54
      -1

      Какие юнит тесты… и зачем они нужны этому приложению?


  1. keystore
    15.11.2023 11:54
    +3

    Вроде как лучше надо так: сначала

    COPY ./requirements.txt /app

    RUN pip install

    А потом уже копировать исходники, чтобы не скачивать зависимости при каждом изменении исходников


  1. spookinfish
    15.11.2023 11:54
    +1

    познаваательно


  1. vasya3
    15.11.2023 11:54
    +1

    После:

    Написание веб-приложения:...
    1. Создать файл main.py и запустить его

    тупик. touch main.py nano main.py ?


    1. Aloxiy Автор
      15.11.2023 11:54

      Чтобы создать файл, нужно написать

      touch main.py

      Дальше, с помощью любого текстового редактора запустить файл, текстовый редактор gedit имеет интерфейс, поэтому можно написать

      gedit main.py

      Но если хочешь, можно использовать и nano


  1. sh-vasily
    15.11.2023 11:54

    Почему в post методе передается не объект, а поля по отдельности в качестве аргументов?


  1. unstopppable
    15.11.2023 11:54

    Вместо global обьявить можно так:

    app.get_ids = []

    И все методы спокойно могут обращаться без глобалок


  1. sh-vasily
    15.11.2023 11:54

        except IndexError:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='item does not exists')

    Здесь всего в двух строках содержится уйма проблем:
    1. BAD_REQUEST здесь не подходит. Холиварный вопрос, конечно, но BAD_REQUEST обычно означает, что клиент неправильно составил запрос(к примеру, передал строку вместо числа), здесь же клиент передал число(иначе fast api сам автоматически обработает некорректные данные). И вот допустим, кто-то захочет использовать это API и задумается, почему, когда он делает GET /items/1 все нормально, а когда GET /items/2 - возвращается BAD_REQUEST. Понятно, что догадаться можно, на практике приходилось сталкиваться и с куда более ужасными API, но это все таки обучающая статья, и она должна придерживаться принятых стандартов.
    2. Действительно, fast api умеет преобразовывать исключения в response c соответствующим кодом, но это сделано больше для защиты от необработанных исключений, так что это не повод возвращать любой код, отличный от 2XX. Лучше возвращать Response. Исключения на то и исключения, чтобы выражать через них только экстраординарные ситуации, которых по идее возникнуть не должно, а здесь элемент не найден - вполне стандартная ситуация. Вот пример: https://github.com/sh-vasily/nosql-2023/blob/74073a6095fe1675e12be12d0e2f2f85936dc4ee/router/students_router.py#L26
    3. Тоже в тему исключений: зачем вы отлавливаете IndexError, если можно сразу вначале проверить, есть ли значение с таким индексом? IndexError это ошибка, при этом то, что если клиент запросил по индексу, которого нет, это вполне ожидаемо. Исключения нельзя использовать для выражения ожидаемого поведения, они нарушают структуру кода, создавая множество скрытых точек выхода, что затрудняет чтение и изучение кода. Может быть в реальности на очень маленьком проекте, где весь код умещается в одной функции можно забить, но в больших приложениях это чревато проблемами. И опять же я бы понял, если бы это сделал стартап, где нужно срочно успеть сделать новую фичу для инвесторов и нет времени наводить красоту кода, но здесь учебная статья, зачем учить людей писать плохой код?


    1. Next
      15.11.2023 11:54

      По п.3 конкретно в python это же считается нормальным, типа

      try:
          os.makedirs(new_dir)
      except FileExistsError:
          pass


      1. sh-vasily
        15.11.2023 11:54

        Хм, не буду спорить, потому что на питоне в крупных проектах не участвовал. Но конкретно при работе с массивом я бы предпочел все таки if'ом индекс проверить)