Документация Engee

Внешний программный интерфейс доступа к Engee

Engee предоставляет разработчикам сторонних приложений программный интерфейс (HTTP API), позволяющий стороннему приложению авторизоваться в Engee и совершать действия от имени пользователя в системе Engee.

Авторизация

Авторизация пользователя стороннего приложения в Engee осуществляется через стандартный механизм OAuth.

Разработчик стороннего приложения передает следующую информацию разработчику Engee:

  • redirect_uri — URL в стороннем приложении, куда будет возвращен пользователь.

Разработчик Engee передает разработчику стороннего приложения следующую информацию:

  • client_id — ID стороннего приложения;

  • client_secret — секретный ключ стороннего приложения;

  • scope — авторизованные API для данного приложения.


Как это работает?

  1. Стороннее приложение направляет пользователя на Authorize URL (https://engee.com/account/authorize) со следующими query параметрами:

  2. После того как пользователь даст согласие на предоставление данных, его перенаправит обратно в стороннее приложение на redirect_uri, со следующими query параметрами:

  3. После этого необходимо получить токен. Для этого нужно послать POST запрос на Token URL (https://engee.com/account/api/oauth2/token), со следующим телом (application/x-www-form-urlencoded):

    • grant_type: "authorization_code" — фиксированное значение;

    • code — полученный на втором шаге код;

    • client_id — ID стороннего приложения;

    • client_secret — секретный ключ стороннего приложения;

    • scope — авторизованные API для данного приложения;

    • redirect_uri — URL в стороннем приложении, куда будет возвращен пользователь;

      Пример запроса:

      curl -d grant_type=authorization_code -d code=$code -d client_id=$client_id -d client_secret=$client_secret -d "scope=user:id user:username profile:name profile:primary_email engee" -d redirect_uri=$redirect_uri https://engee.com/account/api/oauth2/token

      В ответ придет access token, которым авторизуются все последующие запросы к HTTP API. Также придет refresh token, который необходим для обновления access token.

    Для обновления токена необходимо послать запрос на адрес POST https://engee.com/account/api/oauth2/token c параметрами:

    • grant_type: "refresh_token" — фиксированное значение;

    • refresh_token — токен обновления;

    • client_id — ID стороннего приложения;

    • client_secret — секретный ключ стороннего приложения;

    • scope — авторизованные API для данного приложения;

    • redirect_uri — URL в стороннем приложении, куда будет возвращен пользователь.

      Время жизни access token: 10 минут, refresh token: 30 дней.
  4. Далее возможно получить данные пользователя с помощью полученного токена, для этого необходимо отправить запрос GET с заголовком (Header) на https://engee.com/account/api/oauth2/session.

    Пример запроса:

    curl -H "Authorization: Bearer $token" https://engee.com/account/api/oauth2/session

    Пример ответа:

    {"sub":"1d592c69-9664-4eb5-9166-447c421a02df","nickname":"username","email":"username@email.tld"}

Взаимодействие с Engee

Для работы всех API по взаимодействию с Engee нужно, чтобы система была запущена у пользователя (нажата кнопка Старт Engee в личном кабинете). Если нет возможности нажимать кнопку запуска, то можно также автоматизировать запуск и остановку Engee (см. описание методов управления Engee далее).

Во все запросы должен быть добавлен заголовок авторизации "Authorization: Bearer $token" (токен получен на этапе авторизации).

Методы API

Префикс для всех API Управления Engeehttps://engee.com.

Управление Engee

GET /account/api/engee/info — получение информации о состоянии Engee. Тело ответа:

{
"username": "string",
"serverName": "string",
"serverId": "string",
"url": "string",
"serverStatus": "starting"|"running"|"stopping"|"stopped",
"startingAt": "2025-02-10T10:24:31.328Z",
"startedAt": "2025-02-10T10:24:31.328Z",
"stoppingAt": "2025-02-10T10:24:31.328Z",
"stoppedAt": "2025-02-10T10:24:31.328Z",
"lastActivityAt": "2025-02-10T10:24:31.328Z",
"inactivityTimeout": 0,
"stopSessionAt": "2025-02-10T10:24:31.328Z",
"clusterNamespace": {
"minimalMode": true
}

POST /account/api/engee/start — запуск Engee (аналог нажатия кнопки Старт Engee в личном кабинете). Тело ответа (URL запускаемого сервера):

{
'server': string
}

DELETE /account/api/engee/stop — остановка Engee. Возвращает код ответа 204.

Префикс для всех остальных API имеет вид: https://engee.com/prod/user/$license-$nickname/. Префикс у каждого пользователя свой, его нужно брать из ответа состояния Engee из поля URL.

Исполнение произвольного кода

POST /external/command/eval

  1. Простейший запрос. Запрос:

    {
      "command": "3 + 5"
    }

    Возвращаемые данные:

    • Статус-код: 200.

      {
        "result": "8"
      }
  2. Ошибка при исполнении исходного кода. Запрос:

    {
      "command": "unexisting_variable"
    }

    Возвращаемые данные:

    • Статус-код: 400.

      {
        "result_code": "command_error",
        "error": {
          "full_description": "UndefVarError: `unexisting_variable` not defined\n\nStacktrace:\n  [1] top-level scope\n    @ none:1\n  [2] eval(m::Module, e::Any)\n    @ Core ./boot.jl:370\n  [3] top-level scope\n    @ none:4\n  [4] eval\n    @ ./boot.jl:370 [inlined]\n  [5] eval_mask_code(code::String)\n    @ Main.Masks /app/IJulia/preload_cells/masks.jl:206\n  [6] eval_mask_codes(mask_codes::Vector{Main.Masks.MaskCode})\n    @ Main.Masks /app/IJulia/preload_cells/masks.jl:197\n  [7] |>\n    @ ./operators.jl:907 [inlined]\n  [8] evaluate_mask_codes(mask_codes::Vector{Dict{String, String}})\n    @ Main.Masks /app/IJulia/preload_cells/masks.jl:272\n  [9] |>\n    @ ./operators.jl:907 [inlined]\n [10] (::Workspaces.Servers.var\"#eval_mask_codes#157\"{typeof(Main.Masks.evaluate_mask_codes), Workspaces.Servers.var\"#convert_mask_result#156\"})(blocks::Vector{Dict{String, String}})\n    @ Workspaces.Servers /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/handlers.jl:539\n [11] request_pipeline(req::HTTP.Messages.Request, log_func::var\"#13#14\"{String}, req_params_func::Function, log_response_func::Workspaces.Servers.var\"#mask_log_and_response#159\"{Workspaces.Servers.var\"#result_to_response#158\"}, pipeline_stages::Workspaces.Servers.PipelineStages)\n    @ Workspaces.Servers /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/handlers_common.jl:282\n [12] required_body_request_pipeline\n    @ /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/handlers_common.jl:289 [inlined]\n [13] eval_mask_code_handler\n    @ /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/handlers.jl:556 [inlined]\n [14] (::Workspaces.Servers.var\"#eval_mask_code#186\"{var\"#13#14\"{String}, typeof(Main.Masks.evaluate_mask_codes)})(req::HTTP.Messages.Request)\n    @ Workspaces.Servers /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/Servers.jl:68\n [15] (::HTTP.Handlers.Router{typeof(HTTP.Handlers.default404), typeof(HTTP.Handlers.default405), Nothing})(req::HTTP.Messages.Request)\n    @ HTTP.Handlers /usr/local/julia-1.9.3/packages/HTTP/sJD5V/src/Handlers.jl:439\n [16] (::HTTP.Handlers.var\"#1#2\"{HTTP.Handlers.Router{typeof(HTTP.Handlers.default404), typeof(HTTP.Handlers.default405), Nothing}})(stream::HTTP.Streams.Stream{HTTP.Messages.Request, HTTP.Connections.Connection{Sockets.TCPSocket}})\n    @ HTTP.Handlers /usr/local/julia-1.9.3/packages/HTTP/sJD5V/src/Handlers.jl:58\n [17] #invokelatest#2\n    @ ./essentials.jl:819 [inlined]\n [18] invokelatest\n    @ ./essentials.jl:816 [inlined]\n [19] handle_connection(f::Function, c::HTTP.Connections.Connection{Sockets.TCPSocket}, listener::HTTP.Servers.Listener{Nothing, Sockets.TCPServer}, readtimeout::Int64, access_log::Nothing)\n    @ HTTP.Servers /usr/local/julia-1.9.3/packages/HTTP/sJD5V/src/Servers.jl:469\n [20] macro expansion\n    @ /usr/local/julia-1.9.3/packages/HTTP/sJD5V/src/Servers.jl:401 [inlined]\n [21] (::HTTP.Servers.var\"#16#17\"{HTTP.Handlers.var\"#1#2\"{HTTP.Handlers.Router{typeof(HTTP.Handlers.default404), typeof(HTTP.Handlers.default405), Nothing}}, HTTP.Servers.Listener{Nothing, Sockets.TCPServer}, Set{HTTP.Connections.Connection}, Int64, Nothing, ReentrantLock, Base.Semaphore, HTTP.Connections.Connection{Sockets.TCPSocket}})()\n    @ HTTP.Servers ./task.jl:514",
          "short_description": "UndefVarError(:unexisting_variable)",
          "type": "UndefVarError"
        }
      }

      здесь:

      • error.full_description — полное описание ошибки вместе со стектрейсом. Вывод аналогичен выводу ошибки в терминал;

      • error.short_description — исключение без стектрейса;

      • error.type — тип ошибки.

  3. Возврат JSON-а. Запрос:

    {
      "command": "using JSON\nJSON.json(\"a\"=>1)"
    }

    Возвращаемые данные:

    • Статус-код: 200.

      {
        "result": "{\"a\":1}"
      }

Загрузка файлов

POST /external/file/upload

Запрос отправляется в формате multipart/form-data.

Поле paths содержит JSON следующего формата:

{
  "sources": [
    "/user/some_dir"
  ]
}

здесь:

  • Каждый элемент внутри sources означает директорию, куда будет загружен файл;

  • Поле upload_files содержит файл, который необходимо загрузить;

  • Количество полей upload_files должно соответствовать количеству переданных директорий.

Пример запроса:

curl -X 'POST' \
  'https://engee.com/prod/user/$license-$user/external/file/upload' \
  -H 'Authorization: Bearer $access_token' \
  -H 'Content-Type: multipart/form-data' \
  -F 'paths={
  "sources": [
    "/user/some_directory"
  ]
}' \
  -F 'upload_files=@2.png;type=image/png'
  • Если директория, куда загружается файл, не существует, то она будет создана;

  • Все скачиваемые файлы должны находиться внутри директории /user, иначе вернется ошибка 403;

  • Если файл уже существует, то к его имени добавится суффикс;

  • Файл будет записан на диск только после полной загрузки. Если загрузка будет прервана, то файл не появится в файловой системе.

Скачивание файлов

POST /external/file/download

Запрос:

{
  "sources": [
    "/user/some_file"
  ]
}
  • Если файл один, то он возвращается в формате multipart/form-data;

  • Если файлов несколько, то они сжимаются в ZIP и уже возвращается application/x-zip-compressed;

  • Если хотя бы один файл не существует, то в ответ вернется 404;

  • Все скачиваемые файлы должны находиться внутри директории /user, иначе вернется ошибка 403.

Асинхронное исполнение кода

Для выполнения кода, время исполнения которого может превышать таймаут HTTP-соединения, используйте асинхронный API. Этот подход позволяет избежать обрыва соединения при выполнении длительных операций.

Выбор между синхронным и асинхронным выполнением:

  • Используйте POST /external/command/eval для быстрого выполнения коротких команд, когда требуется немедленный ответ.

  • Используйте асинхронный API (описан в разделе Создание фоновой задачи) для длительных расчетов, а проверить результат выполнения можно позже.

Создание фоновой задачи

POST /external/command/jobs/create

Запускает выполнение кода в фоновом режиме. Сервер принимает задачу и немедленно возвращает ответ, не дожидаясь завершения выполнения кода.

Тело запроса (application/json):

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "job_type": "EVAL",
  "code": "a = 5; sleep(60); a * 2",
  "name": "Мой расчет"
}

Параметры:

  • job_id — уникальный идентификатор задачи (UUID).

  • job_type — тип задачи ("EVAL").

  • code — код для выполнения.

  • name — необязательное имя задачи для удобства идентификации.

Ответы:

  • 202 Accepted — задача успешно создана и поставлена в очередь на выполнение.

  • 409 Conflict — задача с указанным job_id уже существует.

    Если отправить запрос с уже существующим job_id, то система вернет статус 409 Conflict и не будет создавать дубликат задачи.

Проверка статуса выполнения

GET /external/command/jobs/{job_id}/result

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

Ответ:

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Мой расчет",
  "job_type": "EVAL",
  "status": "IN_PROGRESS",
  "start_time": "2024-05-20T12:00:00Z",
  "end_time": null,
  "result": null
}

Поля ответа:

  • status — статус выполнения: "IN_PROGRESS" (выполняется) или "DONE" (завершена).

  • start_time — время начала выполнения задачи.

  • end_time — время завершения задачи (заполняется когда status = "DONE").

  • result — содержит результат выполнения или ошибку (заполняется когда status = "DONE").

Формат результата при успешном выполнении:

{
  "type": "success",
  "data": {
    "text": "10"
  }
}

Формат результата при ошибке выполнения:

{
  "type": "error",
  "data": {
    "type": "UndefVarError",
    "short_description": "UndefVarError(:some_variable)",
    "full_description": "UndefVarError: `some_variable` not defined\n\nStacktrace:\n..."
  }
}

Ответы:

  • 200 OK — информация о задаче получена.

  • 404 Not Found — задача с указанным job_id не найдена.

Получение списка задач

GET /external/command/jobs/list

Возвращает список всех задач, созданных пользователем.

Ответ:

{
  "jobs": [
    {
      "job_id": "16763be4-6022-406e-a950-fcd5018633ca",
      "name": "task_1",
      "job_type": "EVAL",
      "status": "DONE",
      "start_time": "2024-05-20T12:00:00Z",
      "end_time": "2024-05-20T12:05:00Z",
      "result": {
        "type": "success",
        "data": {
          "text": "Результат выполнения..."
        }
      }
    }
  ]
}

Ответ содержит объект с единственным полем jobs — массивом задач. Для каждой задачи возвращается полная информация включая:

  • Уникальный идентификатор (job_id).

  • Статус выполнения (status).

  • Временные метки начала и окончания.

  • Результат выполнения (если задача завершена).