Начальная конфигурация web-приложения на Python и Starlette

programming

Опубликован:
2024-11-28T00:04:55.855733Z
Отредактирован:
2024-11-28T00:04:55.855733Z
Статус:
публичный
27
0
0

На начальном этапе разработки web-приложения на Python и Starlette, о чём и пойдёт речь далее в этом выпуске нашего уютного блога, мне предстоит выполнить конфигурацию ASGI приложения, определить в нём хотя бы один url-адрес с обработчиком запросов на нём, запустить это приложение и получить доступ к этому url в самом обычном web-браузере. Выполнять все необходимые манипуляции я буду на заранее подготовленном рабочем месте с установленным и настроенным соответствующим образом Debian sid. Пожалуй приступим...

Базовые элементы конфигурации сети

Как известно, современный web описывает взаимодействие клиента и сервера в сети по протоколу HTTP, не устану всякий раз повторять этот тезис, ибо в нём содержатся ответы на многие вопросы. И поскольку мне нужно реализовать такое взаимодействие — описать его кодом, мне потребуется сеть. Сеть на домашнем десктопе... Где будем брать?

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

$ ip a

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

g8mTuUigk6.png

Как видно на снимке экрана выше, первым в этом списке следует интерфейс с именем lo, и для него уже определён IP-адрес подключения, 127.0.0.1. Получить от этого интерфейса отклик мне поможет следующая команда, ping.

$ ping -c 5 127.0.0.1

Выхлоп этой команды покажет, что сетевой интерфейс великолепно откликается по своему IP, а если ещё и заглянуть в системный файл /etc/hosts, то мы увидим, что к этому IP-адресу привязано имя хоста — localhost.

$ grep '127.0.0.1' /etc/hosts

M9HS2utglp.png

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

Структура каталогов web-приложения

Web-приложение состоит из файлов. Файлов будет много, файлы будут разного формата, в них будет содержаться код приложения, написанный на разных диалектах. Всё это великолепие впоследствии необходимо поддерживать, дорабатывать и развивать. А чтобы у выполняющих эти операции людей не получилось каши в голове, на начальном этапе разработки следует продумать структуру каталогов, в которых можно упорядочить хранение файлов по используемым форматам, диалектам и назначению. Фронтенду фронтендово, бэкенду бэкендово, и никак иначе. С чего начнём..?

На домашнем десктопе обычно хранится достаточно много всякого разного. Хорошо бы это всё хранимое упорядочить по тематике. И поскольку любое web-приложение в сущности своей является основой для web-сайтов, на своём десктопе я все свои web-разработки храню в одном каталоге с именем sites. В этом каталоге каждое web-приложение хранится в отдельном вложенном каталоге, имя которого совпадает с именем этого web-приложения. Это может быть любое произвольное имя, для этой демонстрации я выберу имя website, и каталог с этим именем буду считать корневым. Внутри корневого каталога будет храниться базовый каталог текущего web-приложения и различные вспомогательные файлы и каталоги. Базовый каталог я назову webapp. Создать корневой и базовый каталоги можно с помощью команды mkdir прямо в терминале.

$ mkdir -p sites/website/webapp

В итоге я имею корневой каталог с именем website и вложенный в него базовый каталог с именем webapp. Вхожу в корневой каталог.

$ cd sites/website

В базовом каталоге (webapp) будет храниться код web-приложения упорядоченный по диалектам и назначению. Поскольку я пишу приложение на языке программирования Python, создаю в этом каталоге файл с именем __init__.py.

$ touch webapp/__init__.py

Рядом с этим файлом создаю каталог с именем main, в нём будет храниться код главной подпрограммы web-приложения, и это будут файлы с расширением .py, написанные на языке программирования Python.

$ mkdir webapp/main

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

Внутри каталога с именем main создаю файл с именем __init__.py, так в Python обычно определяются пакеты.

$ touch webapp/main/__init__.py

С помощью команды tree можно увидеть, как на текущий момент выглядит дерево корневого каталога.

rz71ZpvkKa.png

На текущий момент разработки мы имеем следующее:

  • website — корневой каталог приложения;

  • webapp — базовый каталог приложения;

  • main — каталог главной подпрограммы приложения.

Корневой и базовый каталоги следует научиться различать, эти термины я буду употреблять в этом цикле статей часто. На следующих этапах разработки я создам в этой структуре другие ключевые каталоги для хранения файлов других форматов, всё это по мере необходимости.

Виртуальное окружение

В процессе разработки приложения website я буду использовать сторонний код на Python, достаточно много разнообразных тематических библиотек, разработанных другими авторами. Все эти библиотеки будет необходимо правильно установить. Работая в операционной системе Debian, можно использовать два различающихся способа установки сторонних библиотек. Сторонние библиотеки можно установить непосредственно в систему из официального хранилища пакетов Debian с помощью пакетного менеджера apt. Но в этом случае мы привязываем приложение к официальному хранилищу конкретного вендора, и что будем делать, если в нём не окажется необходимой библиотеки, не совсем ясно. Второй путь предполагает установку сторонних библиотек из индекса PyPI, и мне он представляется более гибким решением, позволяющим в случае необходимости перенести приложение с Debian на любую другую операционную систему.

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

$ python3 -m venv venv

Здесь ключ -m определяет имя исполняемого модуля, в данном случае это имя venv. Последний аргумент команды — venv в конце команды, определяет имя каталога, в котором будет храниться созданное в результате исполнения этой команды виртуальное окружение.

Важное замечание: чтобы предложенная команда сработала, в операционной системе должен быть установлен пакет python3-venv.

В итоге, в корневом каталоге появится ещё один вложенный каталог — каталог с только что созданным виртуальным окружением. И я получаю возможность его активировать.

$ source venv/bin/activate

21SAfmJ6G6.png

Как видно на снимке экрана выше, в итоге активации только что созданного виртуального окружения у приглашения командной строки появился префикс (venv) — это имя активного в этом терминале виртуального окружения. А я получаю возможность установить в это виртуальное окружение любой доступный в индексе PyPI пакет и использовать его в своём web-приложении.

Установка сторонних библиотек Python

В рамках этого цикла статей я собрался писать web-приложение с помощью и на основе web-фреймворка общего назначения, который называется Starlette. Эта библиотека со свободной лицензией опубликована в индексе PyPI, и я могу установить её в своё текущее виртуальное окружение вот такой простой командой.

$ pip install starlette

Выхлоп этой команды достаточно многословен, и я не буду его приводить здесь полностью, менеджер пакетов Pip загрузит из сети Интернет все необходимые пакеты, включая зависимости, и распакует их в заданные сценарием установки каталоги, а выполнение каждого шага сопроводит отчётом в терминал. В самом конце выхлопа должна содержаться строка Successfully installed и перечень всех установленных этой командой пакетов.

С помощью инструментов Starlette уже в ближайшей перспективе я создам экземпляр ASGI приложения, которое запустить можно с помощью соответствующего ASGI сервера. Возможных вариантов таких серверов известно несколько. В процессе разработки мне понадобится отладочный сервер, и я буду использовать Uvicorn, он вполне успешно справится с моими текущими задачами. Эта библиотека тоже содержится в индексе PyPI, и установить её также просто.

$ pip install 'uvicorn[standard]'

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

Конфигурация ASGI-приложения

Настало время обзавестись собственным экземпляром Starlette, для этого открываю файл __init__.py в текстовом редакторе. Поскольку я работаю в консоли и использую консольный редактор, снимать пальцы с основной позиции на клавиатуре нет необходимости. Вбиваю в приглашение командной строки следующую команду.

$ vim webapp/__init__.py

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

Первым делом мне необходимо создать экземпляр ASGI приложения. Подключаю в этот модуль необходимые мне объекты из только что установленной библиотеки starlette с помощью соответствующих процедур импорта.

from starlette.applications import Starlette
from starlette.routing import Route
from starlette.types import Receive, Scope, Send

Класс Starlette определяет ASGI приложение, с его помощью я могу создать и некоторым образом дополнить собственный экземпляр. Класс Route определяет url-адрес ASGI приложения, с его помощью я буду создавать в приложении точки доступа и привязывать к ним соответствующие обработчики. Объекты Receive, Scope и Send являются вспомогательными, на них будут ссылки в аннотациях.

Разрабатываемое приложение должно быть гибким, для этого ему потребуется файл настроек, который я создам чуть позже. На текущем этапе разработки я определю в карте url-адресов этого приложения всего одну точку доступа — стартовую страницу сайта. Для этой точки доступа мне понадобится обработчик, и он будет храниться в другом модуле, и даже в другом пакете. Дописываю в текущий "инит" ещё пару процедур.

from .dirs import settings
from .main.views import show_index

На данный момент модуль dirs не существует, как не существует и объект settings. То же самое можно сказать и о модуле views из пакета main. Эти файлы и объекты в них я создам чуть позже.

Базовый библиотечный класс Starlette в процессе разработки этого web-приложения мне будет необходимо некоторым образом модифицировать, но редактировать библиотечные файлы не принято, поэтому я создам собственный класс, который будет наследовать от библиотечного класса Starlette. Этому классу даю имя StApp, и переопределяю его метод __call__. Вписываю код этого класса в открытый в текстовом редакторе файл.

class StApp(Starlette):
    async def __call__(
            self, scope: Scope, receive: Receive, send: Send) -> None:
        scope["app"] = self
        self.config = settings
        if self.middleware_stack is None:
            self.middleware_stack = self.build_middleware_stack()
        await self.middleware_stack(scope, receive, send)

На данный момент модификация затронула только свойство config, к нему я привязал ранее подключенный объект settings, которого на данный момент не существует, чуть позже я его создам.

Имея модифицированный класс StApp, уже прямо сейчас я могу создать собственный экземпляр этого класса — app.

app = StApp(
    debug=settings.get('DEBUG', cast=bool),
    routes=[
        Route('/', show_index, name='index')])

Здесь следует обратить внимание на две мелкие детали...

  1. Параметр debug и присвоенное ему значение, оно будет определяться содержанием файла настроек.

  2. Параметр routes и первый элемент в этом списке, который является экземпляром библиотечного класса Route. В нём первый параметр определяет url-адрес стартовой страницы, к которому привязан указанный вторым параметром обработчик — функция представления show_index, она импортирована из модуля views пакета main и на данный момент не существует.

Сохраняю изменения в файл. И вот как код только что отредактированного "инита" выглядит в моём текстовом редакторе.

lkY6KUG1VS.png

Чтобы этот код заработал без ошибок и исключений, необходимо создать уже подключенные в "инит" объекты settings и show_index. Начнём с первого.

У текстового редактора Vim, как известно, есть несколько режимов работы, в том числе командный режим. Чтобы создать и открыть в нём новый файл, достаточно перейти в командный режим и ввести следующую команду.

:tabnew webapp/dirs.py

Несложно догадаться, что сейчас я буду конструировать и править файл dirs.py из базового каталога web-приложения.

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

import os

from starlette.config import Config

base = os.path.dirname(__file__)
settings = Config(os.path.join(os.path.dirname(base), '.env'))

Здесь следует обратить внимание на второй параметр объекта settings, строку .env, она будет определять имя файла настроек, и храниться файл с этим именем будет в корневом каталоге приложения, рядом с базовым каталогом.

Сохраняю изменения в файл. Создаю новый файл с именем .env, ввожу в командном режиме Vim следующую команду.

:tabnew .env

Редактирую этот файл. На текущий момент файл настроек будет содержать одну единственную строчку.

DEBUG=True

Параметр DEBUG содержит булево значение, как определено в "ините".

uT1X4F3maM.png

Функцию представления с именем show_index я создам в новом файле, в полном соответствии с ранее вписанной в "инит" процедурой импорта. Ввожу в командном режиме Vim следующую команду.

:tabnew webapp/main/views.py

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

from starlette.responses import HTMLResponse


async def show_index(request):
    template = '<div>Сайт в стадии разработки.</div>'
    return HTMLResponse(template)

Сохраняю изменения в файл. Вот как это выглядит в тестовом редакторе на моём рабочем десктопе.

yjZfSR6HiY.png

В моих ожиданиях, после запуска приложения и отправки GET запроса на стартовую страницу приложения, сервер должен ответить страницей с HTML кодом, который хранится в объекте template функции представления show_index. На повестке остаётся один единственный вопрос... Как только что спроектированный код запустить и протестировать?

Конфигурация отладочного сервера

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

В окне текстового редактора, в его командном режиме ввожу следующую команду.

:tabnew runserver.py

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

import uvicorn

if __name__ == '__main__':
    uvicorn.run(
        'webapp:app', host='127.0.0.1',
        reload=True, port=5000, log_level='info')

Здесь всё просто... В стандартной идиоме Питона содержится одна единственная процедура вызова библиотечной функции run с заданными параметрами. Здесь следует обратить внимание на параметр host — об этом IP адресе мы говорили в самом начале этой демонстрации, на этом адресе отладочный сервер будет слушать сеть, и к нему можно будет подключиться из любого клиента, например, браузера. Второй заслуживающий внимания параметр — port, для этого конкретного приложения я буду использовать порт 5000. ASGI серверу конечно же нужен экземпляр соответствующего приложения, его я задал первым параметром, он имеет вид webapp:app. Здесь webapp — имя базового каталога, а app — имя экземпляра приложения, который определён в "ините" базового каталога.

l0nDTKt58n.png

Соответственно, я ожидаю, что по адресу localhost:5000 только что созданный сервер откликнется и вернет HTML-страницу с определённым в соответствующей функции представления шаблоном.

Давайте посмотрим на дерево корневого каталога приложения в конечном для данной демонстрации итоге.

dEUtQfLWcL.png

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

Тестирование в браузере

Итак, у меня есть приложение и отладочный сервер, в котором я могу это приложение запустить и протестировать предложенные решения. Покидаю текстовый редактор Vim, и в окне этого же терминала, а в нём активно виртуальное окружение, пробую исполнить файл runserver.py интерпретатором Python активного виртуального окружения.

$ python runserver.py

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

uTSPGIxGjo.png

На снимке экрана видно, что мои запланированные ожидания исполнились, и сервер на GET запрос клиента (браузера) вернул HTML-страницу заданного содержания. Давайте бросим взгляд в окно терминала и посмотрим на отчёт отладочного сервера.

VOPqwmXxxF.png

Как видно на снимке экрана выше, в выхлопе сервера после запроса в браузере стартовой страницы web-приложения появились две строчки, описывающие полученные сервером запросы. Оба запроса с методом GET, но у первого статус код ответа 200, а у второго статус код ответа 404, что неудивительно, ибо в карте url-адресов приложения пока содержится только один адрес — адрес стартовой страницы. Браузер же на автомате запрашивает у сервера иконку для избранного, которой в web-приложении пока не определено, в результате чего сервер отвечает соответствующим статус кодом, иконка не найдена. Эту досадную неприятность я обязательно устраню на предстоящих этапах проектирования, которые планирую описать в следующих выпусках этого блога. А пока отладочный сервер можно остановить сочетанием ctrl+c, так как все цели этой демонстрации полностью достигнуты.

Подводим промежуточный итог

Web-приложение в самом общем случае довольно сложный продукт с весьма трудоёмким, длительным и запутанным процессом разработки, оно включает в себя серверный код, клиентский код, дизайн, вёрстку и прочие непонятные неосведомлённому пользователю составляющие. В этой статье я описал только самый начальный этап проектирования web-приложения на языке программирования Python с помощью инструментов Starlette. Имея собственный экземпляр ASGI приложения и отладочный сервер, я могу без труда продолжить разработку, и далее в этом цикле статей я планирую описать многие аспекты этой работы более скрупулёзно и детально. Насколько далеко я зайду в реализации этих планов, зависит от активности в блоге — ваши посещения, подписки, лайки, комментарии, донаты служат главным мотивирующим меня фактором. Все статьи цикла доступны в хронологическом порядке по метке webapp. Не оставайтесь в стороне, продолжение следует, будет интересно...