Создание полного Fast-API сервиса с фронтендом и деплоем за полчаса

от автора

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

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

Что мы будем использовать?

  1. Python фреймворк FastApi (если с ним совсем незнакомы, то можете найти в моем профиле на Хабре подробное описание всех основных аспектов фреймворка)

  2. Сервис WebSim, который сгенерирует для нас фронтенд. Подробное описание этого бесплатного сервиса и то, как им пользоваться я описывал в этой статье: «WebSim AI: Бесплатный ИИ-помощник для быстрой веб-разработки»

  3. Библиотеку CurlFetch2Py, которая будет выполнять основную логику нашего приложения. Подробное описание библиотеки и того какие она проблемы решает я описывал тут: «CurlFetch2Py – Эффективное преобразование CURL и FETCH команд в структурированные Python объекты»

Что будет делать наше приложение?

Наше приложение будет принимать на входе CURL или FETCH строку и будет трансформировать ее в Python код. Пользователям на выбор мы дадим получение кода под работу с Python библиотекой Requests (синхронная) или с библиотекой HTTPX (асинхронная).

Логика такая:

  1. Выбираем CURL/FECTH на входе

  2. Вставляем строку

  3. Выбираем Requests/httpx

  4. Нажимаем на кнопку

  5. Забираем Python код

И, когда с вводной частью определились, начнем писать код.

Создаем API для приложения

Для начала установим необходимые для проекта библиотеки. Я предлагаю сразу использовать файл requirements.txt, тем более он нам понадобиться при деплое. Заполним его так:

fastapi[all] requests httpx curl_fetch2py
  • requests и httpx предлагаю использовать для тестирования полученного от веб-приложения кода. Устанавливать не обязательно.

  • curl_fetch2py библиотека, которая будет выполнять основную логику по трансформации CURL/FETCH в Python код.

  • fastapi[all] нужен для установки fastapi со всеми зависимостями. К примеру, такая конструкция подтянет Pydantic, который мы будем использовать для валидации данных.

Для установки воспользуемся командой:

pip install -r requirements.txt

Теперь начнем писать само API.

Структуру я буду использовать ту же самую, что и во всех статьях по FastApi.

Подготовим следующие файлы.

 project/ ├── app/ │   ├── api/ │   │   ├── router.py │   │   ├── schemas.py │   │   ├── utils.py │   ├── static/ │   │   ├── script.js │   │   ├── style.css │   ├── templates/ │   │   ├── index.html │   ├── main.py ├── requirements.txt

Теперь нам необходимо подготовит Pydantic модель для обработки входящих данных. Модель мы опишем в файле app/api/schemas.py:

from enum import Enum from pydantic import BaseModel, Field   class StartEnum(str, Enum):     fetch = "fetch"     curl = "curl"   class TargetEnum(str, Enum):     requests = "requests"     httpx = "httpx"   class RequestData(BaseModel):     request_type: StartEnum = Field(..., description="Вариант fetch или curl")     target: TargetEnum = Field(..., description="Вариант requests или httpx")     data_str: str = Field(..., description="Строка на вход")

Модель достаточно простая, если вы знакомы с синтаксисом Pydantic. Единственное что заслуживает внимания – это использование Enum (перечислений).

Я заложил в коде 2 варианта request_type, ограничив их fetch и curl. Кроме того, прописано ограничение для цели конвертации. Тут, так же, 2 варианта: reuests и httpx.

Кроме того, на вход мы будем ждать одно строковое поле. Тут будет содержаться или Fecth-срока или Curl-строка.

Теперь нам необходимо прописать одну утилиту под curl_fetch2py. Дело в том, что библиотека позволяет только получить Python-объект с данными, а нам нужно получить в виде строки код запроса (requsts/httpx).

Утилиту мы опишем в файле app/api/utils.py:

def execute_request(context, target):     method = context.method.upper()     url = context.url     headers = dict(context.headers) if context.headers else None     if isinstance(headers, dict):         try:             del headers['Accept-Encoding']         except:             pass      data = dict(context.data) if context.data else None     cookies = dict(context.cookies) if context.cookies else None      if target == "httpx":         return f'''import httpx import asyncio   async def fetch():     async with httpx.AsyncClient() as client:         response = await client.request(             method="{method}",             url="{url}",             headers={headers},             data={data},             cookies={cookies}         )         return response.text   rez = asyncio.run(fetch()) print(rez) '''     elif target == "requests":         return f'''import requests  def fetch():     response = requests.request(         method="{method}",         url="{url}",         headers={headers},         data={data},         cookies={cookies}     )     return response.text   rez = fetch() print(rez) '''     else:         raise ValueError("Unsupported target")

Как вы видите, на вход данная утилита принимает context – результат выполнения логики curl_fetch2py и target – конечную цель трансформации.

Далее, при работе с обычными f-строками мы будем получать тот результат, который мы будем возвращать пользователям, после того как они воспользуются нашим сервисом.

Значение ключа «Accept-Encoding» из headers я удалил намеренно, так как это значение может помешать нам получать корректный результат в Python после выполнения полученных запросов.

Теперь мы полностью готовы для написания нашего первого и единственного API-метода. Опишем его в файле app/api/router.py:

from fastapi import APIRouter, HTTPException from curl_fetch2py import CurlFetch2Py from app.api.schemas import RequestData from app.api.utils import execute_request  router = APIRouter(prefix='', tags=['API'])   @router.post('/api', summary='Основной API метод') async def main_logic(request_body: RequestData):     request_type = request_body.request_type     target = request_body.target     data_str = request_body.data_str      try:         if request_type == 'curl':             context = CurlFetch2Py.parse_curl_context(data_str)         elif request_type == 'fetch':             context = CurlFetch2Py.parse_fetch_context(data_str)         else:             raise ValueError("Unsupported start type")         return {"request_string": execute_request(context, target).strip()}     except Exception as e:         raise HTTPException(status_code=500, detail=str(e))

Давайте разбираться.

Все начинается с импортов.

from fastapi import APIRouter, HTTPException

  • APIRouter нам нужен для создания роутера, а HTTPException нужен для обработки ошибкок.

from app.api.schemas import RequestData

from app.api.utils import execute_request

Таким образом мы импортировали нашу Pydantic-модель и утилиту.

from curl_fetch2py import CurlFetch2Py

Тут мы импортировали основной класс библиотеки curl_fetch2py, который будет трансформировать CURL/FETCH строки в Python-объекты.

Затем мы настроили наш роутер:

router = APIRouter(prefix='', tags=['API'])

А теперь давайте отдельно разберем наш эндпоинт:

@router.post('/api', summary='Основной API метод') async def main_logic(request_body: RequestData):     request_type = request_body.request_type     target = request_body.target     data_str = request_body.data_str      try:         if request_type == 'curl':             context = CurlFetch2Py.parse_curl_context(data_str)         elif request_type == 'fetch':             context = CurlFetch2Py.parse_fetch_context(data_str)         else:             raise ValueError("Unsupported start type")         return {"request_string": execute_request(context, target).strip()}     except Exception as e:         raise HTTPException(status_code=500, detail=str(e))

На старте мы привязываем работу данного эндпоинта к адресу /api.

Далее, для удобства работы, я достал значения request_type, target и data_str в отдельные переменные, которые мы после будем использовать в коде.

И далее, в зависимости от типа входящих данных, мы получаем Python-объект (контекст) и, если мы не получили никаких ошибок, возвращаем данные объект в виде JSON (словаря) под значение ключа request_string.

Само значение request_string мы прогоняем через нашу утилиту.

Теперь привяжем router к приложению и можно будет приступать к тестам. Для включения роутера необходимо в файле app/main.py прописать следующее:

from fastapi import FastAPI from app.api.router import router as router_api   app = FastAPI() app.include_router(router_api)

Теперь запустим наше FastApi приложение и проверим работает ли наш метод.. Для запуска приложения, с его корня (не папка app), выполним через консоль следующую команду:

uvicorn app.main:app --reload
Если все заполнено корректно, то должны получить похожий результат.

Если все заполнено корректно, то должны получить похожий результат.

Для входа в документацию воспользуемся http://127.0.0.1:8000/docs

Тут мы видим, что нужный нам метод появился. Давайте протестируем его. Для тестирования я буду использовать браузер Firefox и возьму сайт python.org.

  1. Открываем сайт

  2. Вызываем панель разработчика (F12)

  3. Кликаем на вкладу «Сеть»

  4. Обновляем страницу

  5. Находим запрос, который возвращает HTML главной страницы

  6. Кликаем по нему правой кнопкой мыши

  7. Кликаем на «Копировать значение»

  8. Далее нас будет интересовать вариант «Копировать как cURL (POSIX)» и «Копировать как Fetch»

Давайте протестируем как наш метод обрабатывает cURL строки.

Заполним данные следующим образом:

  • request_type: curl

  • target: requests: (после попробуем httpx)

  • data_str: строка CURL, которую мы копировали.

Для проверки нажмем на «Execute».

Я получил корректный результат. FETCH результат протестируем отдельно на форме, так как нам необходима будет некоторая дополнительная обработка, для трансформации многострочной строки в однострочную (эту логику за нас напишет нейронка WebSim).

Приступаем к созданию фронта

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

Далее я опишу каждый свой промт (запрос к нейросети) и при помощи скриншотов покажу какой результат я получал на том или ином этапе. Под каждым скрином описание того промта, который я оправлял WebSim.

ПРОМТ 1

веб-интерфейс для сервиса, который будет трансформировать curl/fetch запросы в python код. Работать должен в 2 форматах: трансформация в requests запрос или в httpx запрос. Логика для сервиса (библиотека) уже написана. Интересует только фронт.

веб-интерфейс для сервиса, который будет трансформировать curl/fetch запросы в python код. Работать должен в 2 форматах: трансформация в requests запрос или в httpx запрос. Логика для сервиса (библиотека) уже написана. Интересует только фронт.

ПРОМТ 2

раздели общий экран приложения на 2 вкладки (2 таба) с выбором curl или fetch. так же добавь больше стилей. тени возле формы, более стилизованные кнопки. В поле результата при успешном результате добавь кнопку для копирования полученной строки.

раздели общий экран приложения на 2 вкладки (2 таба) с выбором curl или fetch. так же добавь больше стилей. тени возле формы, более стилизованные кнопки. В поле результата при успешном результате добавь кнопку для копирования полученной строки.

ПРОМТ 3

Реализуй дизайн веб-приложения в стиле python логотипа. Добавь больше теней сделав все формы более объемными.

Реализуй дизайн веб-приложения в стиле python логотипа. Добавь больше теней сделав все формы более объемными.

ПРОМТ 4

Сделай фон более нейтральным.

Сделай фон более нейтральным.

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

Теперь попросим нейросеть адаптировать JavaScript код приложения под наше существующее API.

ПРОМТ 5

после клика на кнопку "CONVERT TO PYTHON" должен выполнится POST запрос на /api. передаем JSON c такими полями {

  "request_type": "fetch",

  "target": "requests",

  "data_str": "string"

} при формировании data_str необходимо убедиться, что попадает именно однострочная строка, а не многострочная. затем необходимо в поле результата отобразить значение ключа request_string.

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

Далее нам достаточно сохранить полученный результат. Для этого воспользуемся специальным функционалом сервиса WebSim.

Напоминаю, что на этапе подготовки проекта мы с вами заложили такие файлы как: index.html, style.css и script.js.

Разложим полученный HTML на отдельные файлы. В результате у меня получился следующий HTML:

<html> <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1.0">     <title>Python-style cURL/Fetch Converter</title>     <link rel="stylesheet" type="text/css" href="/static/style.css"> </head> <body>   <div class="container">     <h1>Python-style cURL/Fetch Converter</h1>     <div class="tabs">       <div class="tab active" onclick="switchTab('curl')">cURL</div>       <div class="tab" onclick="switchTab('fetch')">Fetch</div>     </div>     <textarea id="input" placeholder="Enter your cURL or fetch command here..."></textarea>     <div class="button-container">       <button class="clear-button" onclick="clearInput()">Clear Input</button>       <button onclick="convertToPython()">Convert to Python</button>       <div class="switch-container">         <span class="slider-label">requests</span>         <label class="switch">           <input type="checkbox" id="libraryToggle">           <span class="slider"></span>         </label>         <span class="slider-label">httpx</span>       </div>     </div>     <div id="output"></div>   </div>    <div class="copy-alert" id="copyAlert">Copied to clipboard!</div>  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script> <script src="/static/script.js"></script> </body> </html>

 В него, отдельно, я прописал импорт JavaScript (script.js):

<script src="/static/script.js"></script>

И стилей (style.css):

<link rel="stylesheet" type="text/css" href="/static/style.css">

Стили и JS прописал в отдельных файлах. Тут дублировать не буду, кому нужен будет полный исходник – переходите в мой телеграмм канал «Легкий путь в Python», там вы найдете не только полный исходный код данного проекта, но и получите эксклюзивный контент, который я не публикую на Хабре.

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

Для начала привяжем обработчик статических файлов (style.css и script.js). Для этого в файле app/main.py нам необходимо прописать следующее:

from fastapi import FastAPI from fastapi.staticfiles import StaticFiles  from app.api.router import router as router_api  app = FastAPI()  app.mount('/static', StaticFiles(directory='app/static'), 'static')  app.include_router(router_api)

Вы видите, что тут мы примонтировали статические файлы. О том как это работает и зачем я подробно писал в статье «Создание собственного API на Python (FastAPI): Подключаем фронтенд и статические файлы».

Теперь нам осталось описать вызов index.html при переходе в корень нашего приложения. Для этого изменим код файла app/api/router.py следующим образом:

from fastapi import APIRouter, Request, HTTPException, Depends from fastapi.templating import Jinja2Templates from curl_fetch2py import CurlFetch2Py from app.api.schemas import RequestData from app.api.utils import execute_request  router = APIRouter(prefix='', tags=['API']) templates = Jinja2Templates(directory='app/templates')   @router.get('/') async def get_main_page(request: Request):     return templates.TemplateResponse(name='index.html', context={'request': request})   @router.post('/api', summary='Основной API метод') async def main_logic(request_body: RequestData):     request_type = request_body.request_type     target = request_body.target     data_str = request_body.data_str      try:         if request_type == 'curl':             context = CurlFetch2Py.parse_curl_context(data_str)         elif request_type == 'fetch':             context = CurlFetch2Py.parse_fetch_context(data_str)         else:             raise ValueError("Unsupported start type")         return {"request_string": execute_request(context, target).strip()}     except Exception as e:         raise HTTPException(status_code=500, detail=str(e))

Тут мы привязали рендер, который будет подтягивать наш index.html и будет возвращать его при переходе в корень приложения.

Перезапустим приложение и перейдем по адресу: http://127.0.0.1:8000/

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

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

Видим, что все работает, но, кое-что нам точно нужно подправить.

Вернемся на сайт WebSim и опишем что бы мы хотели изменить в нашем дизайне или добавить. У меня получился такой промт:

  • При смене вкладки CURL/FETCH пусть очищается поле ввода данных.

  • Добавь кнопку для очистки строки ввода

  • добавь визуальный перенос в результате, если ширина полученной строки превышает ширину экрана демонстрации. При этом влияния на ответ быть не должно (при копировании)

  • стилизуй кнопку копирования, чтоб появлялся не обычный alert, a html

  • стилизуй поле результата и поле ввода.

Затем я прикинул, что приложением может будут и с телефона пользоваться и попросил следующее:

  • добавь адаптацию под мобильную версию

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

Сохраняем, снова разбиваем код на куски и выполним несколько тестов обновленного приложения.

И cURL и FETCH корректно отрабатывают, так как мы это закладывали, а значит, что все работает корректно.

А теперь, для того, чтоб получить практическую пользу от приложения, предлагаю подготовить сложный requests запрос сайта, который без корректной передачи куки не будет возвращать правильные данные.

Для этого я возьму сайт DNS, скопирую в него CURL строку, трансформирую ее в requests запрос через наше веб-приложение и после посмотрю на результат выполнения.

Вставляю полученные данные и копирую результат:

В Pycharm у меня получился такой результат:

Я его немного изменю, а именно, не просто распечатаю результат, а сохраню его в HTML файл. Подпишем как request_curl.html

rez = asyncio.run(fetch()) with open('request_curl.html', 'w', encoding='utf-8') as result:     result.write(rez)

Видим, что данные получены.

Теперь трансформируем curl в httpx и повторим попытку.

Получаем тот-же результат!

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

В качестве такого сервиса, уже не в первый раз, я выбираю Amvera Cloud.

Деплой приложения на Amvera Cloud

Основная причина, по которой я выбираю этот сервис – это его простота. Вам достаточно закинуть файлы в сервис через GIT или напрямую, через консоль, и сервис сам все подхватит и выполнит запуск.

Единственное, что для того, чтоб сервис понял, что именно вы будете запускать, ему необходимо прописать инструкции. Тут есть два варианта:

  • Dockerfile – для тех, кто знает, что такое Docker

  • amvera.yml – файл с простыми инструкциями, который можно сгенерировать прямо на сайте Amvera.

Далее я покажу, как используя amvera.yml файл и GIT мы выполним деплой за 5 минут. Засекайте.

  1. Переходим на сайт Amvera Cloud

  2. Выполняем простую регистрацию, если ее ещё не было (новые пользователи получают 111 рублей на баланс, чего будет достаточно для пользования сервисом пару недель, так как ценник более чем доступный)

  3. Переходим в раздел проектов

  4. Создаем новый проект

  • Тут остановлю внимание на последнем этапе – формирование файла настроек. Настройки заполните, примерно, как на скрине ниже. Тут самое важное – это корректно указать название файла requirements.txt, так как система Amvera должна будет понимать какие библиотеки необходимо устанавливать.

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

Что примечательно, заморочки с Nginx/Apache, как и с htpps протоколом, сервис берет на себя, а вам остается только подготовить файлы и закинуть их в сервис.

Далее, перейдите на вкладку «Репозиторий». Там вас будет интересовать git-ссылка на ваш репозиторий.

Копируем ссылку и на локальной машине, последовательно, выполняем следующие команды (предварительно устанавливаем GIT на локальный компьютер):

git init git remote add amvera ptoject_link

В моем случае это:

git remote add amvera https://git.amvera.ru/yakvenalex/curl-fetch2py

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

Далее вам необходимо забрать файл настроек приложения. Для этого используйте:

git pull amvera master

Файл настроек должен иметь такой вид:

--- meta:   environment: python   toolchain:     name: pip     version: 3.12 build:   requirementsPath: requirements.txt run:   persistenceMount: /data   containerPort: 8000   command: uvicorn app.main:app --host 0.0.0.0 --port 8000 

Тут важно, чтоб порт контейнера совпадал с портом, который вы используете в своей команде (command). Если тут вы видите, что порты отличаются – исправьте это в файле настроек.

После этого отправим приложения на сервис Amvera.

git add . git commit -m "init commit" git push amvera master

После этого файлы должны будут оказаться в сервисе.

Далее вам остается подождать 2-3 минуты, перед тем как ваше приложение станет общедоступным по ссылке, которую мы получили на этапе входа в настройки в приложение.

В моем случае такая ссылка: https://curl-fetch2py-yakvenalex.amvera.io/

Перейдем по ссылке и проверим работает ли приложение:

А что там по документации к API:

Все работает, а это значит что наше приложение прошло все этапы разработки и полностью готово!

Заключение

На этом простом примере я постарался показать, что сегодня, с помощью современных инструментов, таких как WebSim, можно создавать полноценные (Full Stack) приложения самостоятельно, даже не будучи опытным фронтенд-разработчиком. Надеюсь, что эта информация оказалась для вас полезной и дала вам важное осознание: используя WebSim и FastAPI, вы можете без особых усилий визуализировать любой свой код.

Полный исходный код этого и других примеров из моих статей вы найдете в моем телеграм-канале «Легкий путь в Python». Кроме того, к каналу привязано активное сообщество, где мы обсуждаем проблемы и вместе решаем, какая статья выйдет следующей.

Если вам понравилась эта статья, не забудьте поставить лайк, оставить приятный комментарий или подписаться на меня. Это бесплатно, но для автора это не просто приятно — это огромная мотивация создавать для вас еще больше полезного и качественного контента.

На этом пока всё. Всего доброго и до новых встреч!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Хотите больше подобных статей про разработку приложений с фронтендом, но с другой логикой?

66.67% Конечно!8
25% Возможно3
8.33% Нет.1

Проголосовали 12 пользователей. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/833588/