В последнее время я опубликовал более десяти крупных статей на тему разработки собственного 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». Кроме того, к каналу привязано активное сообщество, где мы обсуждаем проблемы и вместе решаем, какая статья выйдет следующей.

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

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

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


  1. IvanZaycev0717
    04.08.2024 10:58
    +1

    Хорошая серия статей

    command: uvicorn app.main:app --host 0.0.0.0 --port 8000

    Здесь в production можно указать параметр --workers, если пользователь будет использовать многоядерную VPS. Даже можно написать скрипт для автоматического определения ядер на машине.


    1. yakvenalex Автор
      04.08.2024 10:58

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


    1. 0Bannon
      04.08.2024 10:58

      https://docs.gunicorn.org/en/stable/design.html#how-many-workers

      ( 2 * os.cpu_count() ) + 1


  1. ovchinnikovproger
    04.08.2024 10:58
    +2

    Сложный проект так, конечно, не сделать, но небольшой прототип/демо - реально если не за пол часа, так за час организовать


    1. yakvenalex Автор
      04.08.2024 10:58

      Для большого проекта нужны серьезные знания во фронтенде, но нам, как бэкендерам, достаточно будет и функционала WtbSim. К примеру, можно показать новичку фронтендеру как должно работать наше апи, а заказчику вообще прелесть. Написал API, потратил час на демку. Прикрутил и закрыл проект. А дальше пусть уже фронтендеры возятся)


  1. Andrey_Solomatin
    04.08.2024 10:58

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


    HTTPX подходит и для синхронных.