Работа с внешним API Engee (Personal Access Token)
В примере рассматриваем работу с внешним API Engee с использованием PAT (Personal Access Token) - реализован управляющий класс EngeeManager, функции проверки статуса, запуска, остановки, выполнения команды, загрузки и скачивания файлов.
В этом примере не рассматривается асинхронное исполнение кода, а также реализация доступа к API Engee по OAuth 2.0
Введение
Работа с Engee через внешний API открывает ещё более широкие возможности автоматизации процессов технических расчётов, разработки и оптимизации моделей, тестирования сложных технических систем при помощи компьютерного моделирования, и, самое главное - интеграции с внешними инженерными средами.
Для начала работы необходимо прочесть документацию к API Engee. В ней же указано, как получить Personal Access Token (PAT).
Подготовка окружения
При разработке примера работы использована версия Python 3.13.
Установим необходимые зависимости:
%pip install -r requirements.txt
После получения токена для этого проекта достаточно просто указать его в файле .env, приложенном к проекту. При публикации проекта в публичный репозиторий не забудьте удалить этот файл или добавить его .gitignore, а также указать Ваш PAT в секретах репозитория.
PAT, указанный в .env проекта отредактирован для защиты личных данных.
Реализация EngeeManager
Ниже приведена реализация всех необходимых для работы классов м функций. Дополнительно они снабжены пояснениями, а в сложных для понимания логики местах - комментариями.
import os
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, Union
from dataclasses import dataclass
import requests
import json
from enum import Enum
from dotenv import load_dotenv
class ServerStatus(str, Enum):
RUNNING = "running"
STOPPED = "stopped"
STARTING = "starting"
@dataclass
class EngeeConfig:
"""Конфигурация подключения к Engee."""
pat: str
base_url: str = "https://engee.com"
timeout: float = 36000.0 # Таймаут бездействия по умолчанию, сек
# serverName: str = "demo00000000-engeeid"
class IEngeeAPI(ABC):
"""Абстрактный интерфейс для API Engee."""
@abstractmethod
def get_server_info(self) -> Optional[Dict[str, Any]]:
pass
@abstractmethod
def start_server(self) -> Optional[Dict[str, Any]]:
pass
@abstractmethod
def stop_server(self) -> bool:
pass
@abstractmethod
def execute_command(self, command: str) -> Optional[str]:
pass
@abstractmethod
def upload_file(self, destination_path: str, local_path: str) -> bool:
pass
class EngeeAPI(IEngeeAPI):
"""Реализация API для взаимодействия с Engee."""
def __init__(self, config: EngeeConfig):
self.config = config
self._headers = {"Authorization": f"Bearer {config.pat}"}
def _make_request(self, method: str, url: str, **kwargs) -> Optional[requests.Response]:
"""Внутренний метод для выполнения HTTP-запросов."""
try:
response = requests.request(method, url, headers=self._headers, **kwargs)
return response if response.status_code < 400 else None
except requests.exceptions.RequestException as e:
print(f"Ошибка запроса: {e}")
return None
def get_server_info(self) -> Optional[Dict[str, Any]]:
"""Получить информацию о состоянии сервера."""
info_url = f"{self.config.base_url}/account/api/engee/info"
response = self._make_request("GET", info_url)
return response.json() if response else None
def start_server(self) -> Optional[Dict[str, Any]]:
"""Запустить сервер."""
info = self.get_server_info()
if not info or info.get("serverStatus") != ServerStatus.STOPPED:
print("Сервер не находится в состоянии 'stopped', запуск невозможен.")
return None
start_url = f"{self.config.base_url}/account/api/engee/start"
payload = {
"url": info["url"],
"inactivityTimeout": self.config.timeout
}
headers = {**self._headers, "Content-Type": "application/json"}
response = self._make_request("POST", start_url, json=payload)
return response.json() if response else None
def stop_server(self) -> bool:
"""Остановить сервер."""
stop_url = f"{self.config.base_url}/account/api/engee/stop"
response = self._make_request("DELETE", stop_url)
success = response is not None and response.status_code == 204
if success:
print("Сервер остановлен")
return success
def execute_command(self, command: str) -> Optional[str]:
"""Выполнить команду на сервере."""
info = self.get_server_info()
if not info:
print("Не удалось получить информацию о сервере.")
return None
if info.get("serverStatus") != ServerStatus.RUNNING:
status = info.get("serverStatus")
if status == ServerStatus.STOPPED:
print("Сервер не запущен. Используйте команду `start_server()` для запуска сервера.")
else:
print("Сервер запускается. Повторите попытку позднее.")
return None
server_url = f"{self.config.base_url}{info['url']}"
eval_url = f"{server_url}/external/command/eval"
payload = {"command": command}
headers = {**self._headers, "Content-Type": "application/json"}
response = self._make_request("POST", eval_url, json=payload)
if response and response.status_code == 201:
result = response.json().get("result")
print(result)
return result
else:
print("No result")
return None
def upload_file(self, destination_path: str, local_path: str) -> bool:
"""Загрузить файл на сервер."""
if not os.path.isfile(local_path):
raise FileNotFoundError(f"Файл не найден: {local_path}")
info = self.get_server_info()
if not info:
print("Не удалось получить информацию о сервере.")
return False
if info.get("serverStatus") != ServerStatus.RUNNING:
status = info.get("serverStatus")
if status == ServerStatus.STOPPED:
print("Сервер не запущен. Используйте команду `start_server()` для запуска сервера.")
else:
print("Сервер запускается. Повторите попытку позднее.")
return False
server_url = f"{self.config.base_url}{info['url']}"
upload_url = f"{server_url}/external/file/upload"
filename = os.path.basename(local_path)
files = {
'paths': (None, json.dumps({"sources": [destination_path]})),
'upload_files': (filename, open(local_path, 'rb'))
}
try:
headers = {**self._headers}
response = self._make_request("POST", upload_url, files=files)
if response:
if response.status_code == 201:
print("Успешно загружено")
return True
elif response.status_code == 200:
print(f"Файл с таким именем существует. Загруженному файлу присвоено имя {response.text}")
return True
else:
print(f"Ошибка: {response.status_code} – {response.text}")
return False
finally:
if 'upload_files' in files and hasattr(files['upload_files'][1], 'close'):
files['upload_files'][1].close()
def download(self, remote_path: Union[str, list[str]], local_save_path: str) -> bool:
"""
Универсальный метод скачивания.
:param remote_path: строка (один файл) или список строк (несколько файлов)
:param local_save_path: локальный путь для сохранения (файл или ZIP)
:return: True при успехе, False при ошибке
"""
# Определяем, передан один путь или несколько
if isinstance(remote_path, str):
paths_to_download = [remote_path]
single_file_mode = True
elif isinstance(remote_path, list):
paths_to_download = remote_path
single_file_mode = False
else:
print("Ошибка: remote_path должен быть строкой или списком строк.")
return False
# Проверяем, что все пути начинаются с '/user/'
for path in paths_to_download:
if not path.startswith('/user/'):
print(f"Ошибка: Все пути должны начинаться с '/user/'. Указан: {path}")
return False
info = self.get_server_info()
if not info:
print("Не удалось получить информацию о сервере.")
return False
if info.get("serverStatus") != ServerStatus.RUNNING:
status = info.get("serverStatus")
if status == ServerStatus.STOPPED:
print("Сервер не запущен. Используйте команду `start_server()` для запуска сервера.")
else:
print("Сервер запускается. Повторите попытку позднее.")
return False
server_url = f"{self.config.base_url}{info['url']}"
download_url = f"{server_url}/external/file/download"
payload = {"sources": paths_to_download}
try:
response = self._make_request("POST", download_url, json=payload)
if not response:
print("Запрос на скачивание не удался.")
return False
if response.status_code == 404:
print(f"Один или несколько файлов из списка не существуют на сервере: {paths_to_download}")
return False
if response.status_code == 403:
print(f"Один или несколько файлов нарушают правило: должны быть внутри /user/. Проверьте: {paths_to_download}")
return False
if response.status_code != 200:
print(f"Ошибка при скачивании: {response.status_code} – {response.text}")
return False
# Создаём директорию для сохранения
os.makedirs(os.path.dirname(local_save_path), exist_ok=True)
content_type = response.headers.get('Content-Type', '')
if single_file_mode:
# Скачиваем как один файл
with open(local_save_path, 'wb') as f:
f.write(response.content)
print(f"Файл '{remote_path}' успешно скачан в '{local_save_path}'")
else:
# Скачиваем как ZIP-архив
if 'application/x-zip-compressed' not in content_type and 'zip' not in content_type:
print(f"Предупреждение: Ответ не является ZIP-архивом. Content-Type: {content_type}")
with open(local_save_path, 'wb') as f:
f.write(response.content)
print(f"Архив с файлами успешно скачан в '{local_save_path}'")
return True
except Exception as e:
print(f"Ошибка при скачивании: {e}")
return False
class EngeeManager:
"""Основной класс для управления Engee"""
def __init__(self, config: EngeeConfig):
self.api = EngeeAPI(config)
def get_status(self) -> Optional[ServerStatus]:
"""Получить текущий статус сервера."""
info = self.api.get_server_info()
if info:
status_str = info.get("serverStatus")
try:
return ServerStatus(status_str)
except ValueError:
return None
return None
def start(self) -> Optional[Dict[str, Any]]:
"""Запустить сервер."""
return self.api.start_server()
def stop(self) -> bool:
"""Остановить сервер."""
return self.api.stop_server()
def command(self, command: str) -> Optional[str]:
"""Выполнить команду на сервере."""
return self.api.execute_command(command)
def upload(self, destination_path: str, local_path: str) -> bool:
"""Загрузить файл на сервер."""
return self.api.upload_file(destination_path, local_path)
def download(self, remote_path: Union[str, list[str]], local_save_path: str) -> bool:
"""
Универсальный метод скачивания: один файл или несколько.
:param remote_path: строка или список строк
:param local_save_path: куда сохранить
"""
return self.api.download(remote_path, local_save_path)
def load_config_from_env(env_file: str = ".env") -> EngeeConfig:
"""Загрузить конфигурацию из файла .env."""
try:
load_dotenv()
except ImportError:
print("Библиотека python-dotenv не установлена. Пропускаем загрузку .env.")
pat = os.environ.get("ENGEE_PAT")
if not pat:
raise ValueError("Переменная окружения ENGEE_PAT не найдена.")
return EngeeConfig(pat=pat)
Пример использования
Загрузим конфигурацию из файла .env и создадим образец класса EngeeManager:
config = load_config_from_env()
manager = EngeeManager(config)
Engee остановлен, проверим статус:
print(f"Текущий статус: {manager.get_status()}")
Запуск Engee
Запустим Engee и проверим статус:
manager.start()
print(f"Текущий статус: {manager.get_status()}")
Пока сеанс не запущен, мы не сможем выполнять команды:
manager.command("pwd()")
Повторная попытка запуска также не даст результатов:
manager.start()
Выполнение команд
Сразу после запуска сеанса Engee мы можем отправить команды по API для выполнения:
print(f"Текущий статус: {manager.get_status()}")
pwd = manager.command("pwd()")
engee_version = manager.command("engee.version()")
Загрузка файлов
Загрузим файл на сервер:
upload_status = manager.upload("/user/upload_test/", r'D:\engee_api\upload_test\upload_test.txt')
Фукция возвращает логическое значение True при успешной загрузке. Проверим нахождение загруженного файла на сервере:
files = manager.command('readdir("/user/upload_test/")')
Попытаемся повторно загрузить файл с тем же именем:
upload_status = manager.upload("/user/upload_test/", r'D:\engee_api\upload_test\upload_test.txt')
Содержимое целевой директории на сервере:
files = manager.command('readdir("/user/upload_test/")')
Скачивание файлов
Скачаем один из ранее загруженных файлов в папку на компьютере:
manager.download("/user/upload_test/upload_test_1.txt", r"D:\engee_api\download_test\download_test.txt")
Попытаемся скачать несколько файлов сразу. Вспоминаем, что ранее в переменную files мы записали список файлов в результирующей директории на сервере. Передадим этот список в соответствующую функцию:
manager.download(
files,
r"D:\engee_api\download_test\download_test.zip"
)
Мы получили ожидаемую ошибку - согласно документации мы должны указывать пути скачиваемых файлов, дочерние /user/.
manager.download(
["/user/upload_test/upload_test.txt", "/user/upload_test/upload_test_1.txt"],
r"D:\engee_api\download_test\download_test.zip"
)
Файлы скачаны на компьютер, можно проверить их наличие в папке данного примера.
Остановка Engee
stop_status = manager.stop()
print(f"Текущий статус: {manager.get_status()}")
pwd = manager.command("pwd()")
Заключение
Мы рассмотрели реализацию класса для работы с внешним API Engee по PAT, а также варианты использования и результаты работы функций для проверки статуса, запуска, остановки Engee, отправки команд и загрузки/скачивания файлов.
Однако это не все имеющиеся даже на текущий релиз возможности API Engee - например, мы не рассмотрели в этом примере Асинхронное исполнение кода. Но это тема, которая заслуживает отдельного и подробного рассмотрения.