External programming interface for accessing Engee
Engee provides developers of third-party applications with a programming interface (HTTP API) that allows a third-party application to log in to Engee and perform actions on behalf of the user in the Engee system.
Authorization
Authorization of a user of a third-party application in Engee is carried out through the standard OAuth mechanism.
The developer of the third-party application transmits the following information to the developer Engee:
-
redirect_uri— The URL in the third-party application to which the user will be returned.
The developer Engee transmits the following information to the developer of the third-party application:
-
client_id— ID of the third-party application; -
client_secret— the secret key of a third-party application; -
scope— authorized API for this application.
How does it work?
-
A third-party application directs the user to
Authorize URL(https://engee.com/account/authorize) with the following query parameters:-
client_id— ID of the third-party application; -
redirect_uri— The URL in the third-party application to which the user will be returned; -
response_type— type of response (must becode); -
scope— authorized API for this application; -
state— the data of the third-party application, which will be returned in the same form when the user is redirected toredirect_uri.
-
-
After the user agrees to provide the data, he will be redirected back to the third-party application on
redirect_uri, with the following query parameters:-
scope— authorized API for this application; -
code— authorization code; -
state— parameterstatepassed to Engee in the first step.Example of a redirect page with query parameters: http://redirect_uri/?code=code&scope;=user%3Aid%20user%3Ausername%20profile%3Aname%20profile%3Aprimary_email%20engee.
-
-
After that, you need to get a token. To do this, you need to send
POSTrequest forToken URL(https://engee.com/account/api/oauth2/token), with the following body (application/x-www-form-urlencoded):-
grant_type: "authorization_code"— fixed value; -
code— the code received in the second step; -
client_id— ID of the third-party application; -
client_secret— the secret key of a third-party application; -
scope— authorized API for this application; -
redirect_uri— The URL in the third-party application to which the user will be returned;Request example:
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/tokenIn response, you will receive an access token that authorizes all subsequent requests to the HTTP API. You will also receive a refresh token, which is required to update the access token.
To update the token, you must send a request to
POSThttps://engee.com/account/api/oauth2/token with parameters:-
grant_type: "refresh_token"— fixed value; -
refresh_token— an update token; -
client_id— ID of the third-party application; -
client_secret— the secret key of a third-party application; -
scope— authorized API for this application; -
redirect_uri— The URL in the third-party application to which the user will be returned.Access token lifetime: 10 minutes, refresh token: 30 days.
-
-
Next, it is possible to get user data using the received token, for this you need to send a request.
GETwith the title (Header) on https://engee.com/account/api/oauth2/session.Request example:
curl -H "Authorization: Bearer $token" https://engee.com/account/api/oauth2/sessionResponse example:
{"sub":"1d592c69-9664-4eb5-9166-447c421a02df","nickname":"username","email":"username@email.tld"}
Interaction with Engee
In order for all APIs to interact with Engee, the system must be started by the user (the Start Engee button is pressed in the personal account). If it is not possible to press the start button, then you can also automate the start and stop of Engee (see the description of Engee control methods below).
The authorization header "Authorization: Bearer $token" must be added to all requests (the token was received at the authorization stage).
API Methods
Prefix for all Management APIs Engee — https://engee.com.
Engee Management
GET /account/api/engee/info — getting information about the status of Engee. Response body:
{
"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 — launch Engee (analogous to pressing the Start Engee button in your personal account). Response body (URL of the server being started):
{
'server': string
}
DELETE /account/api/engee/stop — stop Engee. Returns the response code 204.
The prefix for all other APIs has the form: https://engee.com/prod/user/$license-$nickname/. Each user has their own prefix, which must be taken from the Engee status response from the URL field.
|
Execution of arbitrary code
POST /external/command/eval
-
The simplest request. Request:
{ "command": "3 + 5" }Returned data:
-
Status code: 200.
{ "result": "8" }
-
-
An error occurred when executing the source code. Request:
{ "command": "unexisting_variable" }Returned data:
-
Status code: 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" } }here:
-
error.full_description— a full description of the error along with the stack trace. The output is similar to the error output to the terminal; -
error.short_description— exception without stack trace; -
error.type— type of error.
-
-
-
JSON file return. Request:
{ "command": "using JSON\nJSON.json(\"a\"=>1)" }Returned data:
-
Status code: 200.
{ "result": "{\"a\":1}" }
-
Uploading files
POST /external/file/upload
The request is sent in the format multipart/form-data.
Field paths contains JSON in the following format:
{
"sources": [
"/user/some_dir"
]
}
here:
-
Each element inside
sourcesindicates the directory where the file will be uploaded.; -
Field
upload_filescontains the file that needs to be uploaded; -
Number of fields
upload_filesit must match the number of transferred directories.
Request example:
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'
-
If the directory where the file is uploaded does not exist, it will be created.;
-
All downloaded files must be located inside the directory.
/userotherwise, error 403 will be returned; -
If the file already exists, then a suffix will be added to its name.;
-
The file will be written to disk only after full download. If the download is interrupted, the file will not appear in the file system.
Downloading files
POST /external/file/download
Request:
{
"sources": [
"/user/some_file"
]
}
-
If there is only one file, it is returned in the format
multipart/form-data; -
If there are several files, they are compressed into ZIP and are already returned.
application/x-zip-compressed; -
If at least one file does not exist, a 404 will be returned in response.;
-
All downloaded files must be located inside the directory.
/userotherwise, error 403 will be returned.
Asynchronous code execution
To execute code whose execution time may exceed the timeout of the HTTP connection, use the asynchronous API. This approach avoids disconnecting the connection when performing long-term operations.
|
The choice between synchronous and asynchronous execution:
|
Creating a background task
POST /external/command/jobs/create
Starts code execution in the background. The server accepts the task and immediately returns a response, without waiting for the code execution to complete.
Request body (application/json):
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"job_type": "EVAL",
"code": "a = 5; sleep(60); a * 2",
"name": "My calculation"
}
Parameters:
-
job_id— unique task identifier (UUID). -
job_type— type of task ("EVAL"). -
code— the code to execute. -
name— optional task name for easy identification.
Answers:
-
202 Accepted— the task has been successfully created and queued for execution. -
409 Conflict— a task with the specifiedjob_idit already exists.If you send a request with an existing one job_id, then the system will return the status409 Conflictand it will not create a duplicate of the task.
Checking the progress status
GET /external/command/jobs/{job_id}/result
Allows you to get the current status of the task and its result (when the task is completed).
Answer:
{
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My calculation",
"job_type": "EVAL",
"status": "IN_PROGRESS",
"start_time": "2024-05-20T12:00:00Z",
"end_time": null,
"result": null
}
Response fields:
-
status— completion status:"IN_PROGRESS"(in progress) or"DONE"(completed). -
start_time— the start time of the task. -
end_time— task completion time (filled in when status ="DONE"). -
result— contains the result of execution or an error (filled in whenstatus = "DONE").
The format of the result upon successful completion:
{
"type": "success",
"data": {
"text": "10"
}
}
Format of the result in case of execution error:
{
"type": "error",
"data": {
"type": "UndefVarError",
"short_description": "UndefVarError(:some_variable)",
"full_description": "UndefVarError: `some_variable` not defined\n\nStacktrace:\n..."
}
}
Answers:
-
200 OK— information about the task has been received. -
404 Not Found— a task with the specifiedjob_idnot found.
Getting a list of issues
GET /external/command/jobs/list
Retrieves the list of all tasks created by the user.
Answer:
{
"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": "The result of the execution..."
}
}
}
]
}
The response contains an object with a single field jobs — an array of tasks. Full information is returned for each task, including:
-
Unique identifier (
job_id). -
Completion status (
status). -
Start and end timestamps.
-
The result of the execution (if the task is completed).