5 Commits

Author SHA1 Message Date
Brett Williams 250aa22557 Update proxy to use similar named methods to new API calls 2026-06-06 01:16:53 -05:00
Brett Williams 95341e815c Remove redundant installed_engines endpoint 2026-06-06 01:03:16 -05:00
Brett Williams f3d469af53 Delete engine API cleanup 2026-06-06 00:54:43 -05:00
Brett Williams b8f025ccba Change api methods to use POST when possible 2026-06-06 00:49:56 -05:00
Brett Williams 076eebcdac Consolidate engine_info api calls 2026-06-06 00:34:48 -05:00
7 changed files with 122 additions and 115 deletions
+22 -11
View File
@@ -83,18 +83,24 @@ The system works by:
Jobs can be submitted via the desktop UI or programmatically via the API:
- **Via UI**: Use the desktop interface to upload project files, specify render settings, and queue jobs.
- **Via API**: Send POST requests to `/api/jobs` with job configuration in JSON format.
- **Via API**: Send `POST` requests to `/api/add_job` with job configuration in JSON format.
Example API request:
```bash
curl -X POST http://localhost:5000/api/jobs \
curl -X POST http://localhost:8080/api/add_job \
-H "Content-Type: application/json" \
-d '{
"engine": "blender",
"project_path": "/path/to/project.blend",
"output_path": "/path/to/output",
"frames": "1-100",
"settings": {"resolution": "1920x1080"}
"name": "example-render",
"engine_name": "blender",
"local_path": "/path/to/project.blend",
"output_path": "example-output",
"start_frame": 1,
"end_frame": 100,
"args": {
"export_format": "PNG",
"resolution": [1920, 1080]
},
"enable_split_jobs": false
}'
```
@@ -103,9 +109,14 @@ curl -X POST http://localhost:5000/api/jobs \
- **UI**: View job status, progress, logs, and worker availability in real-time.
- **API Endpoints**:
- `GET /api/jobs`: List all jobs
- `GET /api/jobs/{id}`: Get job details
- `DELETE /api/jobs/{id}`: Cancel a job
- `GET /api/workers`: List connected workers
- `GET /api/job/<job_id>`: Get job details
- `POST /api/job/<job_id>/cancel`: Cancel a job
- `POST /api/job/<job_id>/delete`: Delete a job
- `GET /api/status`: Get server and queue status
- `GET /api/engines`: List engine information
For the full endpoint reference, see [`docs/api.html`](docs/api.html) or
[`docs/API.md`](docs/API.md).
#### Worker Management
@@ -193,4 +204,4 @@ Zordon is licensed under the MIT License. See the [LICENSE](LICENSE.txt) file fo
## Notice
This software is in beta and intended for casual/hobbyist use. Not recommended for mission-critical environments.
This software is in beta and intended for casual/hobbyist use. Not recommended for mission-critical environments.
+67 -79
View File
@@ -17,6 +17,7 @@ import psutil
import yaml
from flask import Flask, request, send_file, after_this_request, Response, redirect, url_for
from sqlalchemy.orm.exc import DetachedInstanceError
from werkzeug.exceptions import HTTPException
from src.api.job_import_handler import JobImportHandler
from src.api.preview_manager import PreviewManager
@@ -306,7 +307,7 @@ def add_job_handler():
return 'unknown error', 500
@server.get('/api/job/<job_id>/cancel')
@server.post('/api/job/<job_id>/cancel')
def cancel_job(job_id):
if not request.args.get('confirm', False):
return 'Confirmation required to cancel job', 400
@@ -320,7 +321,7 @@ def cancel_job(job_id):
return "Unknown error", 500
@server.route('/api/job/<job_id>/delete', methods=['POST', 'GET'])
@server.post('/api/job/<job_id>/delete')
def delete_job(job_id):
try:
if not request.args.get("confirm", False):
@@ -379,93 +380,38 @@ def get_engine_for_filename():
return f"Error: cannot find a suitable engine for '{filename}'", 400
return found_engine.name()
@server.get('/api/installed_engines')
def get_installed_engines():
result = {}
for engine_class in EngineManager.supported_engines():
data = EngineManager.all_version_data_for_engine(engine_class.name())
if data:
result[engine_class.name()] = data
return result
@server.get('/api/engine_info')
def engine_info():
def _validated_engine_response_type():
response_type = request.args.get('response_type', 'standard')
if response_type not in ['full', 'standard']:
raise ValueError(f"Invalid response_type: {response_type}")
def process_engine(engine):
try:
# Get all installed versions of the engine
installed_versions = EngineManager.all_version_data_for_engine(engine.name())
if not installed_versions:
return None
system_installed_versions = [v for v in installed_versions if v['type'] == 'system']
install_path = system_installed_versions[0]['path'] if system_installed_versions else installed_versions[0]['path']
en = engine(install_path)
engine_name = en.name()
result = {
engine_name: {
'is_available': RenderQueue.is_available_for_job(engine_name),
'versions': installed_versions
}
}
if response_type == 'full':
with concurrent.futures.ThreadPoolExecutor() as executor:
future_results = {
'supported_extensions': executor.submit(en.supported_extensions),
'supported_export_formats': executor.submit(en.get_output_formats),
'system_info': executor.submit(en.system_info)
}
for key, future in future_results.items():
result[engine_name][key] = future.result()
return result
except Exception as e:
traceback.print_exc(e)
logger.error(f"Error fetching details for engine '{engine.name()}': {e}")
return {}
engine_data = {}
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {executor.submit(process_engine, engine): engine.name() for engine in EngineManager.supported_engines()}
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
engine_data.update(result)
return engine_data
return response_type
@server.get('/api/<engine_name>/info')
def get_engine_info(engine_name):
def _engine_info_for_engine(engine_class, response_type='standard'):
try:
response_type = request.args.get('response_type', 'standard')
# Get all installed versions of the engine
installed_versions = EngineManager.all_version_data_for_engine(engine_name)
installed_versions = EngineManager.all_version_data_for_engine(engine_class.name())
if not installed_versions:
return {}
return None
result = { 'is_available': RenderQueue.is_available_for_job(engine_name),
'versions': installed_versions
}
system_installed_versions = [v for v in installed_versions if v['type'] == 'system']
install_path = (
system_installed_versions[0]['path'] if system_installed_versions else installed_versions[0]['path']
)
engine = engine_class(install_path)
engine_name = engine.name()
result = {
'is_available': RenderQueue.is_available_for_job(engine_name),
'versions': installed_versions
}
if response_type == 'full':
with concurrent.futures.ThreadPoolExecutor() as executor:
engine_class = EngineManager.engine_class_with_name(engine_name)
en = EngineManager.get_latest_engine_instance(engine_class)
future_results = {
'supported_extensions': executor.submit(en.supported_extensions),
'supported_export_formats': executor.submit(en.get_output_formats),
'system_info': executor.submit(en.system_info),
'options': executor.submit(en.ui_options)
'supported_extensions': executor.submit(engine.supported_extensions),
'supported_export_formats': executor.submit(engine.get_output_formats),
'system_info': executor.submit(engine.system_info),
'options': executor.submit(engine.ui_options)
}
for key, future in future_results.items():
@@ -473,13 +419,52 @@ def get_engine_info(engine_name):
return result
except Exception as e:
logger.error(f"Error fetching details for engine '{engine_class.name()}': {e}")
return {}
@server.get('/api/engines')
def get_engines_info():
response_type = _validated_engine_response_type()
engine_data = {}
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {
executor.submit(_engine_info_for_engine, engine, response_type): engine.name()
for engine in EngineManager.supported_engines()
}
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
engine_data[futures[future]] = result
return engine_data
@server.get('/api/engines/names')
def get_engine_names():
result = []
for engine_class in EngineManager.supported_engines():
data = EngineManager.all_version_data_for_engine(engine_class.name())
if data:
result.append(engine_class.name())
return result
@server.get('/api/engines/<engine_name>')
def get_engine(engine_name):
try:
response_type = _validated_engine_response_type()
engine_class = EngineManager.engine_class_with_name(engine_name)
return _engine_info_for_engine(engine_class, response_type) or {}
except Exception as e:
logger.error(f"Error fetching details for engine '{engine_name}': {e}")
return {}
@server.get('/api/<engine_name>/is_available')
def is_engine_available(engine_name):
def get_engine_availability(engine_name):
return {'engine': engine_name, 'available': RenderQueue.is_available_for_job(engine_name),
'cpu_count': int(psutil.cpu_count(logical=False)),
'versions': EngineManager.all_version_data_for_engine(engine_name),
@@ -632,6 +617,9 @@ def handle_404(error):
@server.errorhandler(Exception)
def handle_general_error(general_error):
if isinstance(general_error, HTTPException):
return general_error.description, general_error.code
traceback.print_exception(type(general_error), general_error, general_error.__traceback__)
err_msg = f"Server error: {general_error}"
logger.error(err_msg)
@@ -649,7 +637,7 @@ def detected_clients():
return ZeroconfServer.found_hostnames()
@server.get('/api/_debug/clear_history')
@server.post('/api/_debug/clear_history')
def clear_history():
RenderQueue.clear_history()
return 'success'
+22 -16
View File
@@ -109,6 +109,11 @@ class RenderServerProxy:
return requests.get(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout,
headers={"X-API-Version": str(API_VERSION)})
def _post(self, payload, timeout=5, **kwargs):
from src.api.api_server import API_VERSION
return requests.post(f'http://{self.hostname}:{self.port}/api/{payload}', timeout=timeout,
headers={"X-API-Version": str(API_VERSION)}, **kwargs)
# --------------------------------------------
# Background Updates:
# --------------------------------------------
@@ -225,10 +230,10 @@ class RenderServerProxy:
return response
def cancel_job(self, job_id, confirm=False):
return self.request_data(f'job/{job_id}/cancel?confirm={confirm}')
return self._post(f'job/{job_id}/cancel', params={'confirm': confirm})
def delete_job(self, job_id, confirm=False):
return self.request_data(f'job/{job_id}/delete?confirm={confirm}')
return self._post(f'job/{job_id}/delete', params={'confirm': confirm})
def send_subjob_update_notification(self, parent_id, subjob):
"""
@@ -252,15 +257,15 @@ class RenderServerProxy:
response = self.request(f'engine_for_filename?filename={os.path.basename(filename)}', timeout)
return response.text
def get_installed_engines(self, timeout=5):
return self.request_data(f'installed_engines', timeout)
def is_engine_available(self, engine_name:str, timeout=5):
def get_engine_availability(self, engine_name:str, timeout=5):
return self.request_data(f'{engine_name}/is_available', timeout)
def get_all_engine_info(self, response_type='standard', timeout=5):
def get_engine_names(self, timeout=5):
return self.request_data('engines/names', timeout=timeout)
def get_engines(self, response_type='standard', timeout=5):
"""
Fetches all engine information from the server.
Fetches engine information from the server.
Args:
response_type (str, optional): Returns standard or full version of engine info
@@ -269,10 +274,10 @@ class RenderServerProxy:
Returns:
dict: A dictionary containing the engine information.
"""
all_data = self.request_data(f"engine_info?response_type={response_type}", timeout=timeout)
all_data = self.request_data(f'engines?response_type={response_type}', timeout=timeout)
return all_data
def get_engine_info(self, engine_name:str, response_type='standard', timeout=5):
def get_engine(self, engine_name:str, response_type='standard', timeout=5):
"""
Fetches specific engine information from the server.
@@ -284,22 +289,23 @@ class RenderServerProxy:
Returns:
dict: A dictionary containing the engine information.
"""
return self.request_data(f'{engine_name}/info?response_type={response_type}', timeout)
return self.request_data(f'engines/{engine_name}?response_type={response_type}', timeout)
def delete_engine(self, engine_name:str, version:str, system_cpu=None):
def delete_engine_download(self, engine_name:str, version:str, system_os=None, cpu=None):
"""
Sends a request to the server to delete a specific engine.
Sends a request to the server to delete a specific engine download.
Args:
engine_name (str): The name of the engine to delete.
version (str): The version of the engine to delete.
system_cpu (str, optional): The system CPU type. Defaults to None.
system_os (str, optional): The system OS. Defaults to None.
cpu (str, optional): The system CPU type. Defaults to None.
Returns:
Response: The response from the server.
"""
form_data = {'engine': engine_name, 'version': version, 'system_cpu': system_cpu}
return requests.post(f'http://{self.hostname}:{self.port}/api/delete_engine', json=form_data)
form_data = {'engine': engine_name, 'version': version, 'system_os': system_os, 'cpu': cpu}
return self._post('delete_engine', json=form_data)
# --------------------------------------------
# Download Files:
+1 -1
View File
@@ -276,7 +276,7 @@ class DistributedJobManager:
host_properties = ZeroconfServer.get_hostname_properties(hostname)
if host_properties.get('api_version') == API_VERSION:
if not system_os or (system_os and system_os == host_properties.get('system_os')):
response = RenderServerProxy(hostname).is_engine_available(engine_name)
response = RenderServerProxy(hostname).get_engine_availability(engine_name)
if response and response.get('available', False):
found_available_servers.append(response)
+4 -4
View File
@@ -66,7 +66,7 @@ class NewRenderJobForm(QWidget):
# Job / Server Data
self.server_proxy = RenderServerProxy(socket.gethostname())
self.project_info = None
self.installed_engines = {}
self.installed_engines = []
self.preferred_engine = None
# Setup
@@ -345,7 +345,7 @@ class NewRenderJobForm(QWidget):
self.engine_version_combo.addItem('latest')
self.file_format_combo.clear()
if current_engine:
engine_info = self.server_proxy.get_engine_info(current_engine, 'full', timeout=10)
engine_info = self.server_proxy.get_engine(current_engine, 'full', timeout=10)
self.current_engine_options = engine_info.get('options', [])
if not engine_info:
raise FileNotFoundError(f"Cannot get information about engine '{current_engine}'")
@@ -404,7 +404,7 @@ class NewRenderJobForm(QWidget):
"""Called by the GetProjectInfoWorker - Do not call directly."""
try:
self.engine_type.addItems(self.installed_engines.keys())
self.engine_type.addItems(self.installed_engines)
self.engine_type.setCurrentText(self.preferred_engine)
self.engine_changed()
@@ -633,7 +633,7 @@ class GetProjectInfoWorker(QThread):
def run(self):
try:
# get the engine info and add them all to the ui
self.window.installed_engines = self.window.server_proxy.get_installed_engines()
self.window.installed_engines = self.window.server_proxy.get_engine_names()
# select the best engine for the file type
self.window.preferred_engine = self.window.server_proxy.get_engine_for_filename(self.project_path)
+4 -2
View File
@@ -93,7 +93,7 @@ class EngineBrowserWindow(QMainWindow):
def update_table(self):
def update_table_worker():
raw_server_data = RenderServerProxy(self.hostname).get_all_engine_info()
raw_server_data = RenderServerProxy(self.hostname).get_engines()
if not raw_server_data:
return
@@ -158,7 +158,9 @@ class EngineBrowserWindow(QMainWindow):
if reply is not QMessageBox.StandardButton.Yes:
return
result = RenderServerProxy(self.hostname).delete_engine(engine_info['engine'], engine_info['version'])
result = RenderServerProxy(self.hostname).delete_engine_download(
engine_info['engine'], engine_info['version'], engine_info.get('system_os'), engine_info.get('cpu'),
)
if result.ok:
self.update_table()
else:
+2 -2
View File
@@ -37,7 +37,7 @@ class GetEngineInfoWorker(QThread):
self.parent = parent
def run(self):
data = RenderServerProxy(socket.gethostname()).get_all_engine_info()
data = RenderServerProxy(socket.gethostname()).get_engines()
self.done.emit(data)
class SettingsWindow(QMainWindow):
@@ -549,4 +549,4 @@ if __name__ == "__main__":
app = QApplication([])
window = SettingsWindow()
window.show()
app.exec()
app.exec()