diff --git a/README.md b/README.md index 11a7eed..7e62b0c 100644 --- a/README.md +++ b/README.md @@ -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/jobs` 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/jobs \ -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,15 @@ 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 + - `POST /api/jobs`: Submit a new job + - `GET /api/jobs/`: Get job details + - `POST /api/jobs//cancel`: Cancel a job + - `POST /api/jobs//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 +205,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. \ No newline at end of file +This software is in beta and intended for casual/hobbyist use. Not recommended for mission-critical environments. diff --git a/add_job.py b/add_job.py index ec0ece3..9fc9674 100644 --- a/add_job.py +++ b/add_job.py @@ -95,7 +95,7 @@ def main(): new_job = {"name": job_name, "engine_name": args.engine} try: - response = found_proxy.post_job_to_server(file_path, new_job) + response = found_proxy.create_job(file_path, new_job) except Exception as e: print(f"Error creating job: {e}") exit(1) @@ -113,7 +113,7 @@ def main(): while percent_complete < 1.0: # add checks for errors time.sleep(1) - running_job_data = found_proxy.get_job_info(job_id) + running_job_data = found_proxy.get_job(job_id) percent_complete = running_job_data['percent_complete'] sys.stdout.write("\x1b[1A") # Move up 1 sys.stdout.write("\x1b[0J") # Clear from cursor to end of screen (optional) @@ -128,4 +128,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..344adb0 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,487 @@ +# Zordon API Reference + +Zordon exposes a Flask API from `src/api/api_server.py`. The server is started by +`start_api_server()` and listens on `Config.port_number` with all application +routes mounted under `/api`. + +The in-repo client wrapper is `src/api/server_proxy.py`. Most UI and distributed +rendering code should prefer `RenderServerProxy` instead of constructing request +URLs directly. + +## Versioning + +- Current API version: `0.1` +- `RenderServerProxy.request()` sends `X-API-Version` with the current + `API_VERSION`, but the server does not currently validate this header. + +## Response Conventions + +- JSON endpoints return Flask-serialized dictionaries or lists. +- File endpoints return `send_file()` responses. +- Most error responses are plain text with HTTP `400`, `404`, `500`, or `503`. +- `JobNotFoundError` is mapped to HTTP `400`. +- Unhandled exceptions are mapped to HTTP `500` with a plain-text message. + +## Jobs + +### `GET /api/jobs` + +Returns all render jobs and a cache token. + +Response: + +```json +{ + "jobs": [ + { + "id": "job-id", + "name": "job name", + "status": "running" + } + ], + "token": "cache-token" +} +``` + +Known callers: + +- `RenderServerProxy.get_jobs()` +- `src/ui/main_window.py` + +### `GET /api/jobs/long_poll` + +Long-polls the job list until the supplied cache token changes or 30 seconds +elapse. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `token` | No | Cache token returned by `/api/jobs`. | + +Responses: + +- `200` with the same shape as `/api/jobs` when jobs changed. +- `204` with an empty body when no changes arrive before timeout. + +Known callers: + +- `RenderServerProxy.get_jobs()` through the background cache updater. + +### `GET /api/jobs/status/` + +Returns jobs matching a render status. + +Path parameters: + +| Name | Description | +| --- | --- | +| `status_val` | Status string converted by `string_to_status()`. | + +Responses: + +- `200` with a list of job JSON objects when matches exist. +- `400` when no jobs match the requested status. + +Review note: this route is not currently wrapped by `RenderServerProxy` and no +in-repo callers were found. + +### `GET /api/jobs/` + +Returns one job as JSON. + +Known callers: + +- `RenderServerProxy.get_job()` +- `add_job.py` +- `src/ui/main_window.py` +- `src/distributed_job_manager.py` +- `tests/job_creation_tests.py` + +### `GET /api/jobs//logs` + +Returns the job log file as `text/plain`. + +Known callers: + +- `src/ui/main_window.py` opens this URL directly. + +### `GET /api/jobs//files` + +Returns a list of output filenames for the job. + +Known callers: + +- `RenderServerProxy.get_job_files()` +- `src/utilities/server_helper.py` + +### `GET /api/jobs//download` + +Downloads one output file. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `filename` | Yes | Case-insensitive filename from the job file list. | + +Responses: + +- `200` with the requested file as an attachment. +- `400` when `filename` is missing. +- `404` when the file is not found. + +Known callers: + +- `RenderServerProxy.download_job_file()` +- `src/utilities/server_helper.py` + +### `GET /api/jobs//download_all` + +Creates a temporary zip of the job output directory and downloads it. + +Known callers: + +- `RenderServerProxy.download_all_job_files()` +- `src/ui/main_window.py` +- `src/utilities/server_helper.py` + +### `GET /api/jobs//thumbnail` + +Returns a generated preview image or video for a job. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `size` | No | `big` selects the larger preview path. Currently parsed but not applied. | +| `video_ok` | No | If truthy and a video preview exists, video can be returned. | + +Responses: + +- `200` with `image/jpeg` or `video/mp4`. +- `404` when no thumbnail is available. +- `500` on preview generation errors. + +Known callers: + +- `src/ui/main_window.py` + +Review note: `size=big` is parsed into `big_thumb` but not used. + +## Job Lifecycle + +### `POST /api/jobs` + +Adds one or more render jobs. + +Request formats: + +- JSON request body. +- Multipart form with a `json` field and optional `file` upload. + +Common job fields include: + +| Name | Description | +| --- | --- | +| `name` | Display name for the render job. | +| `renderer` | Render engine name such as `blender` or `ffmpeg`. | +| `start_frame` | First frame to render. | +| `end_frame` | Last frame to render. | +| `args` | Engine-specific render arguments. | +| `enable_split_jobs` | Whether distributed subjobs may be created. | +| `child_jobs` | Optional subjob definitions. | +| `local_path` | Local file path used when posting to localhost. | + +Responses: + +- `200` with created job data. +- `400` for invalid or missing job data. +- `500` for unexpected processing or creation errors. + +Known callers: + +- `RenderServerProxy.create_job()` +- `add_job.py` +- `src/ui/add_job_window.py` +- `src/distributed_job_manager.py` +- `tests/job_creation_tests.py` + +### `POST /api/jobs//cancel` + +Cancels a job. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `confirm` | Yes | Must be truthy or the request is rejected. | +| `redirect` | No | If truthy, redirects to `index`. | + +Known callers: + +- `RenderServerProxy.cancel_job()` +- `src/ui/main_window.py` +- `src/distributed_job_manager.py` + +### `POST /api/jobs//delete` + +Deletes a job, stops it first, deletes previews, and removes owned upload/output +directories when safe. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `confirm` | Yes | Must be truthy or the request is rejected. | + +Known callers: + +- `RenderServerProxy.delete_job()` +- `src/ui/main_window.py` + +### `POST /api/jobs//subjob_update` + +Notifies a parent job that a child/subjob changed state. + +Request body: + +- JSON representation of a subjob. + +Known callers: + +- `RenderServerProxy.send_subjob_update_notification()` +- `src/distributed_job_manager.py` + +## Status and Environment + +### `GET /api/heartbeat` + +Returns the current timestamp as plain text. Used for fast connectivity checks. + +Known callers: + +- `RenderServerProxy.check_connection()` + +### `GET /api/status` + +Returns local system and queue status. + +Response includes: + +- timestamp +- operating system and version +- CPU brand, count, and current utilization +- memory totals and current utilization +- job counts +- hostname and port +- app and API versions + +Known callers: + +- `RenderServerProxy.get_status()` + +### `GET /api/presets` + +Returns `config/presets.yaml`. + +Review note: no in-repo callers were found. + +### `GET /api/cpu_benchmark` + +Runs a CPU benchmark for 10 seconds and returns the score as plain text. + +Known callers: + +- `src/utilities/server_helper.py` + +### `GET /api/disk_benchmark` + +Runs a disk I/O benchmark and returns write/read speeds. + +Review note: no in-repo callers were found. + +## Engines + +### `GET /api/engines/for_filename` + +Returns the engine name suitable for a project filename. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `filename` | Yes | Project filename or path. The client currently sends only the basename. | + +Known callers: + +- `RenderServerProxy.get_engine_for_filename()` +- `src/ui/add_job_window.py` + +### `GET /api/engines` + +Returns installed engine data keyed by engine name. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `response_type` | No | `standard` or `full`; defaults to `standard`. | + +`full` responses also include supported extensions, supported export formats, +system info, and UI options. + +Known callers: + +- `RenderServerProxy.get_engines()` +- `src/ui/settings_window.py` +- `src/ui/engine_browser.py` + +### `GET /api/engines/names` + +Returns installed engine names as a list without instantiating engine classes. +Use this for lightweight selection UIs that only need engine names. + +Known callers: + +- `RenderServerProxy.get_engine_names()` +- `src/ui/add_job_window.py` + +### `GET /api/engines/` + +Returns installed version data for a single engine. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `response_type` | No | `standard` or `full`; defaults to `standard`. | + +`full` responses also include supported extensions, supported export formats, +system info, and UI options. + +Known callers: + +- `RenderServerProxy.get_engine()` +- `src/ui/add_job_window.py` + +### `GET /api/engines//availability` + +Returns whether an engine can accept jobs on this server, plus CPU count, +installed versions, and hostname. + +Known callers: + +- `RenderServerProxy.get_engine_availability()` +- `src/distributed_job_manager.py` + +### `GET /api/engines//args` + +Returns engine arguments. + +Review note: no in-repo callers were found. + +### `GET /api/engines//help` + +Returns engine help text. + +Known callers: + +- `src/ui/add_job_window.py` opens this URL directly. + +### `GET /api/engines/download_available` + +Checks whether a managed engine version is available to download. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `engine` | Yes | Engine name. | +| `version` | Yes | Engine version. | +| `system_os` | No | Target OS. | +| `cpu` | No | Target CPU architecture. | + +Review note: no in-repo callers were found. + +### `GET /api/engines/most_recent_version` + +Finds the most recent downloadable version for an engine. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `engine` | Yes | Engine name. | +| `system_os` | No | Target OS. | +| `cpu` | No | Target CPU architecture. | + +Review note: no in-repo callers were found. + +### `POST /api/engines/download` + +Downloads a managed engine version. + +Query parameters: + +| Name | Required | Description | +| --- | --- | --- | +| `engine` | Yes | Engine name. | +| `version` | Yes | Engine version. | +| `system_os` | No | Target OS. | +| `cpu` | No | Target CPU architecture. | + +Review note: no in-repo callers were found. Settings currently calls +`EngineManager.download_engine()` directly instead of this API route. + +### `POST /api/engines/delete` + +Deletes a managed engine download. + +JSON body: + +| Name | Required | Description | +| --- | --- | --- | +| `engine` | Yes | Engine name. | +| `version` | Yes | Engine version. | +| `system_os` | No | Target OS. | +| `cpu` | No | Target CPU architecture. | + +Known callers: + +- `RenderServerProxy.delete_engine_download()` +- `src/ui/engine_browser.py` + +## Debug + +### `GET /api/_debug/detected_clients` + +Returns hostnames detected by Zeroconf. + +Review note: development/debug only, with an inline comment saying it probably +should not ship. + +### `POST /api/_debug/clear_history` + +Clears render queue history and returns `success`. + +Review note: development/debug only. + +## Redundancy and Cleanup Review + +Routes with no in-repo callers found: + +- `GET /api/jobs/status/` +- `GET /api/presets` +- `GET /api/disk_benchmark` +- `GET /api/engines//args` +- `GET /api/engines/download_available` +- `GET /api/engines/most_recent_version` +- `POST /api/engines/download` +- `GET /api/_debug/detected_clients` +- `POST /api/_debug/clear_history` + +Routes or methods with cleanup risks: + +- `job_thumbnail()` parses `size=big` but never uses the resulting `big_thumb` + value. diff --git a/docs/api.html b/docs/api.html new file mode 100644 index 0000000..d9a9c52 --- /dev/null +++ b/docs/api.html @@ -0,0 +1,577 @@ + + + + + +Zordon API Reference + + + +
+

Zordon API Reference

+

Flask endpoints exposed by src/api/api_server.py, with the in-repo client wrapper in src/api/server_proxy.py.

+
+ +
+ + +
+
+

Overview

+
+
+

Versioning

+
    +
  • Current API version: 0.1
  • +
  • RenderServerProxy.request() sends X-API-Version.
  • +
  • The server does not currently validate that header.
  • +
+
+
+

Response Conventions

+
    +
  • JSON endpoints return Flask-serialized dictionaries or lists.
  • +
  • File endpoints return send_file() responses.
  • +
  • Most errors are plain text with 400, 404, 500, or 503.
  • +
  • JobNotFoundError maps to HTTP 400.
  • +
+
+
+
+ +
+

Jobs

+ +
+
GET/api/jobs
+

Returns all render jobs and a cache token.

+
{
+  "jobs": [{"id": "job-id", "name": "job name", "status": "running"}],
+  "token": "cache-token"
+}
+

Known callers:

+
    +
  • RenderServerProxy.get_jobs()
  • +
  • src/ui/main_window.py
  • +
+
+ +
+
GET/api/jobs/long_poll
+

Long-polls the job list until the supplied cache token changes or 30 seconds elapse.

+ + + +
Query ParameterRequiredDescription
tokenNoCache token returned by /api/jobs.
+

Returns 200 with job data when changed, or 204 when no change arrives before timeout.

+
+ +
+
GET/api/jobs/status/<status_val>
+

Returns jobs matching a render status converted by string_to_status().

+
No RenderServerProxy wrapper or in-repo caller was found.
+
+ +
+
GET/api/jobs/<job_id>
+

Returns one job as JSON.

+

Known callers include RenderServerProxy.get_job(), add_job.py, src/ui/main_window.py, src/distributed_job_manager.py, and tests/job_creation_tests.py.

+
+ +
+
GET/api/jobs/<job_id>/logs
+

Returns the job log file as text/plain. The main window opens this URL directly.

+
+ +
+
GET/api/jobs/<job_id>/files
+

Returns a list of output filenames for the job.

+

Known callers: RenderServerProxy.get_job_files() and src/utilities/server_helper.py.

+
+ +
+
GET/api/jobs/<job_id>/download
+

Downloads one output file.

+ + + +
Query ParameterRequiredDescription
filenameYesCase-insensitive filename from the job file list.
+

Returns 200 with an attachment, 400 when filename is missing, or 404 when the file is not found.

+
+ +
+
GET/api/jobs/<job_id>/download_all
+

Creates a temporary zip of the job output directory and downloads it.

+

Known callers: RenderServerProxy.download_all_job_files(), src/ui/main_window.py, and src/utilities/server_helper.py.

+
+ +
+
GET/api/jobs/<job_id>/thumbnail
+

Returns a generated preview image or video for a job.

+ + + + +
Query ParameterRequiredDescription
sizeNobig is parsed but not currently applied.
video_okNoIf truthy and a video preview exists, video can be returned.
+
Cleanup note: size=big is parsed into big_thumb but not used.
+
+
+ +
+

Job Lifecycle

+ +
+
POST/api/jobs
+

Adds one or more render jobs. Accepts either a JSON request body or multipart form data with a json field and optional file upload.

+ + + + + + + + + + +
Common FieldDescription
nameDisplay name for the render job.
rendererRender engine name such as blender or ffmpeg.
start_frameFirst frame to render.
end_frameLast frame to render.
argsEngine-specific render arguments.
enable_split_jobsWhether distributed subjobs may be created.
child_jobsOptional subjob definitions.
local_pathLocal file path used when posting to localhost.
+

Known callers include RenderServerProxy.create_job(), add_job.py, src/ui/add_job_window.py, src/distributed_job_manager.py, and integration tests.

+
+ +
+
POST/api/jobs/<job_id>/cancel
+

Cancels a job. Requires a truthy confirm query parameter.

+ + + + +
Query ParameterRequiredDescription
confirmYesMust be truthy or the request is rejected.
redirectNoIf truthy, redirects to index.
+
+ +
+
POST/api/jobs/<job_id>/delete
+

Deletes a job, stops it first, deletes previews, and removes owned upload/output directories when safe.

+ + + +
Query ParameterRequiredDescription
confirmYesMust be truthy or the request is rejected.
+
+ +
+
POST/api/jobs/<job_id>/subjob_update
+

Notifies a parent job that a child/subjob changed state. The request body is the JSON representation of the subjob.

+
+
+ +
+

Status and Environment

+ +
+
GET/api/heartbeat
+

Returns the current timestamp as plain text. Used by RenderServerProxy.check_connection().

+
+ +
+
GET/api/status
+

Returns local system and queue status, including operating system, CPU, memory, job counts, hostname, port, app version, and API version.

+
+ +
+
GET/api/presets
+

Returns config/presets.yaml.

+
No in-repo callers were found.
+
+ +
+
GET/api/cpu_benchmark
+

Runs a CPU benchmark for 10 seconds and returns the score as plain text. Used by src/utilities/server_helper.py.

+
+ +
+
GET/api/disk_benchmark
+

Runs a disk I/O benchmark and returns write/read speeds.

+
No in-repo callers were found.
+
+
+ +
+

Engines

+ +
+
GET/api/engines/for_filename
+

Returns the engine name suitable for a project filename.

+ + + +
Query ParameterRequiredDescription
filenameYesProject filename or path. The client currently sends only the basename.
+
+ +
+
GET/api/engines
+

Returns installed engine data keyed by engine name.

+ + + +
Query ParameterRequiredDescription
response_typeNostandard or full; defaults to standard.
+

full responses include supported extensions, supported export formats, system info, and UI options.

+

Known callers: RenderServerProxy.get_engines(), src/ui/settings_window.py, and src/ui/engine_browser.py.

+
+ +
+
GET/api/engines/names
+

Returns installed engine names as a list without instantiating engine classes. Use this for lightweight selection UIs that only need engine names.

+

Known callers: RenderServerProxy.get_engine_names() and src/ui/add_job_window.py.

+
+ +
+
GET/api/engines/<engine_name>
+

Returns installed version data for a single engine.

+ + + +
Query ParameterRequiredDescription
response_typeNostandard or full; defaults to standard.
+

full responses include supported extensions, supported export formats, system info, and UI options.

+

Known caller: RenderServerProxy.get_engine() in the add-job window.

+
+ +
+
GET/api/engines/<engine_name>/availability
+

Returns whether an engine can accept jobs on this server, plus CPU count, installed versions, and hostname.

+
+ +
+
GET/api/engines/<engine_name>/args
+

Returns engine arguments.

+
No in-repo callers were found.
+
+ +
+
GET/api/engines/<engine_name>/help
+

Returns engine help text. The add-job window opens this URL directly.

+
+ +
+
GET/api/engines/download_available
+

Checks whether a managed engine version is available to download.

+ + + + + + +
Query ParameterRequiredDescription
engineYesEngine name.
versionYesEngine version.
system_osNoTarget OS.
cpuNoTarget CPU architecture.
+
No in-repo callers were found.
+
+ +
+
GET/api/engines/most_recent_version
+

Finds the most recent downloadable version for an engine.

+ + + + + +
Query ParameterRequiredDescription
engineYesEngine name.
system_osNoTarget OS.
cpuNoTarget CPU architecture.
+
No in-repo callers were found.
+
+ +
+
POST/api/engines/download
+

Downloads a managed engine version.

+ + + + + + +
Query ParameterRequiredDescription
engineYesEngine name.
versionYesEngine version.
system_osNoTarget OS.
cpuNoTarget CPU architecture.
+
Settings currently calls EngineManager.download_engine() directly instead of this API route.
+
+ +
+
POST/api/engines/delete
+

Deletes a managed engine download.

+ + + + + + +
JSON FieldRequiredDescription
engineYesEngine name.
versionYesEngine version.
system_osNoTarget OS.
cpuNoTarget CPU architecture.
+
+
+ +
+

Debug

+ +
+
GET/api/_debug/detected_clients
+

Returns hostnames detected by Zeroconf.

+
Development/debug only, with an inline comment saying it probably should not ship.
+
+ +
+
POST/api/_debug/clear_history
+

Clears render queue history and returns success.

+
Development/debug only.
+
+
+ +
+

Cleanup Review

+
+

Routes With No In-Repo Callers Found

+
    +
  • GET /api/jobs/status/<status_val>
  • +
  • GET /api/presets
  • +
  • GET /api/disk_benchmark
  • +
  • GET /api/engines/<engine_name>/args
  • +
  • GET /api/engines/download_available
  • +
  • GET /api/engines/most_recent_version
  • +
  • POST /api/engines/download
  • +
  • GET /api/_debug/detected_clients
  • +
  • POST /api/_debug/clear_history
  • +
+
+ +
+

Cleanup Risks

+
    +
  • job_thumbnail() parses size=big but never uses the resulting big_thumb value.
  • +
+
+
+
+
+ + diff --git a/server.py b/server.py index 5b4192c..429fbd4 100755 --- a/server.py +++ b/server.py @@ -58,15 +58,15 @@ class ZordonServer: # ---- Render Queue ---- self.ctx.render_queue = RenderQueue() - self.ctx.render_queue.load_state(database_directory=Path(Config.upload_folder).expanduser()) RenderQueue._default_instance = self.ctx.render_queue RenderQueue._sync_class() + RenderQueue.load_state(database_directory=Path(Config.upload_folder).expanduser()) # ---- Distributed Job Manager ---- self.ctx.distributed_job_manager = DistributedJobManager() - self.ctx.distributed_job_manager.subscribe_to_listener() DistributedJobManager._default_instance = self.ctx.distributed_job_manager DistributedJobManager._sync_class() + DistributedJobManager.subscribe_to_listener() self.api_server = None self.server_hostname: str = socket.gethostname() diff --git a/src/api/api_server.py b/src/api/api_server.py index 4c48df6..8569b3a 100755 --- a/src/api/api_server.py +++ b/src/api/api_server.py @@ -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 @@ -83,7 +84,7 @@ def jobs_json() -> Dict[str, Any]: return {'jobs': all_jobs, 'token': job_cache_token} -@server.get('/api/jobs_long_poll') +@server.get('/api/jobs/long_poll') def long_polling_jobs(): hash_token = request.args.get('token', None) start_time = time.time() @@ -97,7 +98,7 @@ def long_polling_jobs(): time.sleep(1) -@server.get('/api/jobs/') +@server.get('/api/jobs/status/') def filtered_jobs_json(status_val): state = string_to_status(status_val) jobs = [x.json() for x in RenderQueue.jobs_with_status(state)] @@ -111,7 +112,7 @@ def filtered_jobs_json(status_val): # Job Details / File Handling # -------------------------------------------- -@server.get('/api/job/') +@server.get('/api/jobs/') def get_job_details(job_id): """Retrieves the details of a requested job in JSON format @@ -124,7 +125,7 @@ def get_job_details(job_id): return RenderQueue.job_with_id(job_id).json() -@server.get('/api/job//logs') +@server.get('/api/jobs//logs') def get_job_logs(job_id): """Retrieves the log file for a specific render job. @@ -143,12 +144,12 @@ def get_job_logs(job_id): return Response(log_data, mimetype='text/plain') -@server.get('/api/job//file_list') -def get_file_list(job_id): - return [Path(p).name for p in RenderQueue.job_with_id(job_id).file_list()] +@server.get('/api/jobs//files') +def get_job_files(job_id): + return [Path(p).name for p in RenderQueue.job_with_id(job_id).file_list()] -@server.route('/api/job//download') +@server.route('/api/jobs//download') def download_requested_file(job_id): requested_filename = request.args.get("filename") if not requested_filename: @@ -164,7 +165,7 @@ def download_requested_file(job_id): return f"File '{requested_filename}' not found", 404 -@server.route('/api/job//download_all') +@server.route('/api/jobs//download_all') def download_all_files(job_id): zip_filename = None @@ -205,29 +206,6 @@ def presets() -> Dict[str, Any]: return loaded_presets -@server.get('/api/full_status') -def full_status(): - full_results = {'timestamp': datetime.now().isoformat(), 'servers': {}} - - try: - snapshot_results = snapshot() - server_data = {'status': snapshot_results.get('status', {}), 'jobs': snapshot_results.get('jobs', {}), - 'is_online': True} - full_results['servers'][server.config['HOSTNAME']] = server_data - except Exception as e: - logger.error(f"Exception fetching full status: {e}") - - return full_results - - -@server.get('/api/snapshot') -def snapshot(): - server_status = status() - server_jobs = [x.json() for x in RenderQueue.all_jobs()] - server_data = {'status': server_status, 'jobs': server_jobs, 'timestamp': datetime.now().isoformat()} - return server_data - - @server.route('/api/status') def status(): return {"timestamp": datetime.now().isoformat(), @@ -253,10 +231,10 @@ def status(): # Job Lifecyle (Create, Cancel, Delete) # -------------------------------------------- -@server.post('/api/add_job') -def add_job_handler(): +@server.post('/api/jobs') +def create_jobs_handler(): """ - POST /api/add_job + POST /api/jobs Add a render job to the queue. **Request Formats** @@ -306,7 +284,7 @@ def add_job_handler(): return 'unknown error', 500 -@server.get('/api/job//cancel') +@server.post('/api/jobs//cancel') def cancel_job(job_id): if not request.args.get('confirm', False): return 'Confirmation required to cancel job', 400 @@ -320,7 +298,7 @@ def cancel_job(job_id): return "Unknown error", 500 -@server.route('/api/job//delete', methods=['POST', 'GET']) +@server.post('/api/jobs//delete') def delete_job(job_id): try: if not request.args.get("confirm", False): @@ -369,7 +347,7 @@ def delete_job(job_id): # Engine Info and Management: # -------------------------------------------- -@server.get('/api/engine_for_filename') +@server.get('/api/engines/for_filename') def get_engine_for_filename(): filename = request.args.get("filename") if not filename: @@ -379,93 +357,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//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,32 +396,75 @@ 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/') +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//is_available') -def is_engine_available(engine_name): +@server.get('/api/engines//availability') +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), - 'hostname': server.config['HOSTNAME']} + 'hostname': server.config.get('HOSTNAME', socket.gethostname())} -@server.get('/api/engine//args') +@server.get('/api/engines//args') def get_engine_args(engine_name): try: engine_class = EngineManager.engine_class_with_name(engine_name) + if not engine_class: + return f"Cannot find engine '{engine_name}'", 400 return engine_class().get_arguments() except LookupError: return f"Cannot find engine '{engine_name}'", 400 -@server.get('/api/engine//help') +@server.get('/api/engines//help') def get_engine_help(engine_name): try: engine_class = EngineManager.engine_class_with_name(engine_name) + if not engine_class: + return f"Cannot find engine '{engine_name}'", 400 return engine_class().get_help() except LookupError: return f"Cannot find engine '{engine_name}'", 400 @@ -507,7 +473,7 @@ def get_engine_help(engine_name): # Engine Downloads and Updates: # -------------------------------------------- -@server.get('/api/is_engine_available_to_download') +@server.get('/api/engines/download_available') def is_engine_available_to_download(): available_result = EngineManager.version_is_available_to_download(request.args.get('engine'), request.args.get('version'), @@ -517,7 +483,7 @@ def is_engine_available_to_download(): (f"Cannot find available download for {request.args.get('engine')} {request.args.get('version')}", 500) -@server.get('/api/find_most_recent_version') +@server.get('/api/engines/most_recent_version') def find_most_recent_version(): most_recent = EngineManager.find_most_recent_version(request.args.get('engine'), request.args.get('system_os'), @@ -526,7 +492,7 @@ def find_most_recent_version(): (f"Error finding most recent version of {request.args.get('engine')}", 500) -@server.post('/api/download_engine') +@server.post('/api/engines/download') def download_engine(): download_result = EngineManager.download_engine(request.args.get('engine'), request.args.get('version'), @@ -536,7 +502,7 @@ def download_engine(): (f"Error downloading {request.args.get('engine')} {request.args.get('version')}", 500) -@server.post('/api/delete_engine') +@server.post('/api/engines/delete') def delete_engine_download(): json_data = request.json delete_result = EngineManager.delete_engine_download(json_data.get('engine'), @@ -554,14 +520,14 @@ def delete_engine_download(): def heartbeat(): return datetime.now().isoformat(), 200 -@server.post('/api/job//send_subjob_update_notification') +@server.post('/api/jobs//subjob_update') def subjob_update_notification(job_id): subjob_details = request.json DistributedJobManager.handle_subjob_update_notification(RenderQueue.job_with_id(job_id), subjob_data=subjob_details) return Response(status=200) -@server.route('/api/job//thumbnail') +@server.route('/api/jobs//thumbnail') def job_thumbnail(job_id): try: @@ -632,6 +598,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 +618,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' diff --git a/src/api/server_proxy.py b/src/api/server_proxy.py index d45908f..c0873b4 100644 --- a/src/api/server_proxy.py +++ b/src/api/server_proxy.py @@ -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: # -------------------------------------------- @@ -134,7 +139,7 @@ class RenderServerProxy: if self.__offline_flags: # if we're offline, don't bother with the long poll ignore_token = True - url = f'jobs_long_poll?token={self.__jobs_cache_token}' if (self.__jobs_cache_token and + url = f'jobs/long_poll?token={self.__jobs_cache_token}' if (self.__jobs_cache_token and not ignore_token) else 'jobs' status_result = self.request_data(url, timeout=timeout) if status_result is not None: @@ -153,14 +158,11 @@ class RenderServerProxy: # Get System Info: # -------------------------------------------- - def get_all_jobs(self, timeout=5, ignore_token=False): + def get_jobs(self, timeout=5, ignore_token=False): if not self.__update_in_background or ignore_token: self.__update_job_cache(timeout, ignore_token) return self.__jobs_cache.copy() if self.__jobs_cache else None - def get_data(self, timeout=5): - return self.request_data('full_status', timeout=timeout) - def get_status(self): status = self.request_data('status') if status and not self.system_cpu: @@ -175,17 +177,17 @@ class RenderServerProxy: # Get Job Info: # -------------------------------------------- - def get_job_info(self, job_id, timeout=5): - return self.request_data(f'job/{job_id}', timeout=timeout) + def get_job(self, job_id, timeout=5): + return self.request_data(f'jobs/{job_id}', timeout=timeout) - def get_job_files_list(self, job_id): - return self.request_data(f"job/{job_id}/file_list") + def get_job_files(self, job_id): + return self.request_data(f'jobs/{job_id}/files') # -------------------------------------------- # Job Lifecycle: # -------------------------------------------- - def post_job_to_server(self, file_path: Path, job_data, callback=None): + def create_job(self, file_path: Path, job_data, callback=None): """ Posts a job to the server. @@ -204,7 +206,7 @@ class RenderServerProxy: # Bypass uploading file if posting to localhost if self.is_localhost: job_data['local_path'] = str(file_path) - url = urljoin(f'http://{self.hostname}:{self.port}', '/api/add_job') + url = urljoin(f'http://{self.hostname}:{self.port}', '/api/jobs') headers = {'Content-Type': 'application/json'} return requests.post(url, data=json.dumps(job_data), headers=headers) @@ -218,17 +220,17 @@ class RenderServerProxy: # Create a monitor that will track the upload progress monitor = MultipartEncoderMonitor(encoder, callback) if callback else MultipartEncoderMonitor(encoder) headers = {'Content-Type': monitor.content_type} - url = urljoin(f'http://{self.hostname}:{self.port}', '/api/add_job') + url = urljoin(f'http://{self.hostname}:{self.port}', '/api/jobs') # Send the request with proper resource management with requests.post(url, data=monitor, headers=headers) as response: 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'jobs/{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'jobs/{job_id}/delete', params={'confirm': confirm}) def send_subjob_update_notification(self, parent_id, subjob): """ @@ -241,7 +243,7 @@ class RenderServerProxy: Returns: Response: The response from the server. """ - return requests.post(f'http://{self.hostname}:{self.port}/api/job/{parent_id}/send_subjob_update_notification', + return requests.post(f'http://{self.hostname}:{self.port}/api/jobs/{parent_id}/subjob_update', json=subjob.json()) # -------------------------------------------- @@ -249,18 +251,18 @@ class RenderServerProxy: # -------------------------------------------- def get_engine_for_filename(self, filename:str, timeout=5): - response = self.request(f'engine_for_filename?filename={os.path.basename(filename)}', timeout) + response = self.request(f'engines/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 get_engine_availability(self, engine_name:str, timeout=5): + return self.request_data(f'engines/{engine_name}/availability', timeout) - def is_engine_available(self, engine_name:str, timeout=5): - return self.request_data(f'{engine_name}/is_available', timeout) + def get_engine_names(self, timeout=5): + return self.request_data('engines/names', timeout=timeout) - def get_all_engine_info(self, response_type='standard', timeout=5): + 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 +271,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,33 +286,34 @@ 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('engines/delete', json=form_data) # -------------------------------------------- # Download Files: # -------------------------------------------- def download_all_job_files(self, job_id, save_path): - url = f"http://{self.hostname}:{self.port}/api/job/{job_id}/download_all" + url = f'http://{self.hostname}:{self.port}/api/jobs/{job_id}/download_all' return self.__download_file_from_url(url, output_filepath=save_path) def download_job_file(self, job_id, job_filename, save_path): - url = f"http://{self.hostname}:{self.port}/api/job/{job_id}/download?filename={job_filename}" + url = f'http://{self.hostname}:{self.port}/api/jobs/{job_id}/download?filename={job_filename}' return self.__download_file_from_url(url, output_filepath=save_path) @staticmethod diff --git a/src/distributed_job_manager.py b/src/distributed_job_manager.py index c9198b2..b7fd82f 100644 --- a/src/distributed_job_manager.py +++ b/src/distributed_job_manager.py @@ -172,7 +172,7 @@ class DistributedJobManager: subjob_id = child_key.split('@')[0] subjob_hostname = child_key.split('@')[-1] - subjob_data = RenderServerProxy(subjob_hostname).get_job_info(subjob_id) + subjob_data = RenderServerProxy(subjob_hostname).get_job(subjob_id) if not subjob_data: logger.warning(f"No response from {subjob_hostname}") parent_job.children[child_key]['download_status'] = f'error: No response from {subjob_hostname}' @@ -260,7 +260,7 @@ class DistributedJobManager: subjob['engine_version'] = parent_worker.engine_version logger.debug(f"Posting subjob with frames {subjob['start_frame']}-" f"{subjob['end_frame']} to {server_hostname}") - post_results = RenderServerProxy(server_hostname).post_job_to_server( + post_results = RenderServerProxy(server_hostname).create_job( file_path=project_path, job_data=subjob) return post_results @@ -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) diff --git a/src/ui/add_job_window.py b/src/ui/add_job_window.py index 1e73688..8bd7310 100644 --- a/src/ui/add_job_window.py +++ b/src/ui/add_job_window.py @@ -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}'") @@ -386,7 +386,7 @@ class NewRenderJobForm(QWidget): self.job_name_input.setText(directory) def args_help_button_clicked(self): - url = (f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/engine/' + url = (f'http://{self.server_proxy.hostname}:{self.server_proxy.port}/api/engines/' f'{self.engine_type.currentText()}/help') self.engine_help_viewer = EngineHelpViewer(url) self.engine_help_viewer.show() @@ -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() @@ -608,8 +608,8 @@ class SubmitWorker(QThread): input_path = Path(latest_engine.perform_presubmission_tasks(input_path)) # submit err_msg = "" - result = self.window.server_proxy.post_job_to_server(file_path=input_path, job_data=job_json, - callback=create_callback) + result = self.window.server_proxy.create_job(file_path=input_path, job_data=job_json, + callback=create_callback) if not (result and result.ok): err_msg = f"Error posting job to server: {result.text}" @@ -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) diff --git a/src/ui/engine_browser.py b/src/ui/engine_browser.py index fa815c4..37c0cf7 100644 --- a/src/ui/engine_browser.py +++ b/src/ui/engine_browser.py @@ -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: diff --git a/src/ui/main_window.py b/src/ui/main_window.py index daa3871..c1a6423 100644 --- a/src/ui/main_window.py +++ b/src/ui/main_window.py @@ -334,7 +334,7 @@ class MainWindow(QMainWindow): default_image_path = "error.png" before_fetch_hostname = self.current_server_proxy.hostname - response = self.current_server_proxy.request(f'job/{job_id}/thumbnail?size=big') + response = self.current_server_proxy.request(f'jobs/{job_id}/thumbnail?size=big') if response.ok: try: with io.BytesIO(response.content) as image_data_stream: @@ -547,7 +547,8 @@ class MainWindow(QMainWindow): """ selected_job_ids = self.selected_job_ids() if selected_job_ids: - url = f'http://{self.current_server_proxy.hostname}:{self.current_server_proxy.port}/api/job/{selected_job_ids[0]}/logs' + url = (f'http://{self.current_server_proxy.hostname}:{self.current_server_proxy.port}' + f'/api/jobs/{selected_job_ids[0]}/logs') self.log_viewer_window = LogViewer(url) self.log_viewer_window.show() @@ -562,7 +563,7 @@ class MainWindow(QMainWindow): return if len(job_ids) == 1: - job = next((job for job in self.current_server_proxy.get_all_jobs() if job.get('id') == job_ids[0]), None) + job = next((job for job in self.current_server_proxy.get_jobs() if job.get('id') == job_ids[0]), None) if job: display_name = job.get('name', os.path.basename(job.get('input_path', ''))) message = f"Are you sure you want to stop job: {display_name}?" @@ -591,7 +592,7 @@ class MainWindow(QMainWindow): return if len(job_ids) == 1: - job = next((job for job in self.current_server_proxy.get_all_jobs() if job.get('id') == job_ids[0]), None) + job = next((job for job in self.current_server_proxy.get_jobs() if job.get('id') == job_ids[0]), None) if job: display_name = job.get('name', os.path.basename(job.get('input_path', ''))) message = f"Are you sure you want to delete the job:\n{display_name}?" @@ -616,8 +617,8 @@ class MainWindow(QMainWindow): return import webbrowser - download_url = (f"http://{self.current_server_proxy.hostname}:{self.current_server_proxy.port}" - f"/api/job/{job_ids[0]}/download_all") + download_url = (f'http://{self.current_server_proxy.hostname}:{self.current_server_proxy.port}' + f'/api/jobs/{job_ids[0]}/download_all') webbrowser.open(download_url) def open_files(self, event): @@ -626,7 +627,7 @@ class MainWindow(QMainWindow): return for job_id in job_ids: - job_info = self.current_server_proxy.get_job_info(job_id) + job_info = self.current_server_proxy.get_job(job_id) path = os.path.dirname(job_info['output_path']) launch_url(path) @@ -665,7 +666,7 @@ class BackgroundUpdater(QThread): ZeroconfServer.get_hostname_properties(x)['api_version'] == API_VERSION] if self.window.current_server_proxy: self.window.job_data[self.window.current_server_proxy.hostname] = \ - self.window.current_server_proxy.get_all_jobs(ignore_token=False) + self.window.current_server_proxy.get_jobs(ignore_token=False) self.needs_update = False self.updated_signal.emit() time.sleep(0.05) diff --git a/src/ui/settings_window.py b/src/ui/settings_window.py index 9734e67..f10465a 100644 --- a/src/ui/settings_window.py +++ b/src/ui/settings_window.py @@ -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() \ No newline at end of file + app.exec() diff --git a/src/utilities/server_helper.py b/src/utilities/server_helper.py index 1040b7a..e417ead 100644 --- a/src/utilities/server_helper.py +++ b/src/utilities/server_helper.py @@ -17,7 +17,7 @@ def download_missing_frames_from_subjob(local_job, subjob_id, subjob_hostname): try: local_files = [os.path.basename(x) for x in local_job.file_list()] subjob_proxy = RenderServerProxy(subjob_hostname) - subjob_files = subjob_proxy.get_job_files_list(job_id=subjob_id) or [] + subjob_files = subjob_proxy.get_job_files(job_id=subjob_id) or [] for subjob_filename in subjob_files: if subjob_filename not in local_files: diff --git a/tests/job_creation_tests.py b/tests/job_creation_tests.py index d0dfc79..0abc3ab 100644 --- a/tests/job_creation_tests.py +++ b/tests/job_creation_tests.py @@ -2,6 +2,7 @@ import logging import os import time import unittest +from pathlib import Path from src.api.server_proxy import RenderServerProxy @@ -38,9 +39,8 @@ class SubmissionTestCase(unittest.TestCase): msg=f'Server not reachable at {SERVER_HOST}:{SERVER_PORT}') def test_submit_job(self): - sample_file_path = os.path.join(os.path.dirname(__file__), 'resources', - 'batman_sample.blend') - self.assertTrue(os.path.exists(sample_file_path), + sample_file_path = Path(__file__).parent / 'resources' / 'batman_sample.blend' + self.assertTrue(sample_file_path.exists(), msg=f'Test file not found: {sample_file_path}') sample_job = { @@ -52,8 +52,8 @@ class SubmissionTestCase(unittest.TestCase): 'enable_split_jobs': False, } - response = self.render_server.post_job_to_server( - file_path=sample_file_path, job_list=[sample_job]) + response = self.render_server.create_job( + file_path=sample_file_path, job_data=sample_job) self.assertIsNotNone(response, msg='No response from server') self.assertTrue(response.ok, msg=f'Server returned {response.status_code}') @@ -68,7 +68,7 @@ class SubmissionTestCase(unittest.TestCase): file_count = 0 while True: - update_response = self.render_server.get_job_info(self.__class__.test_job_id) + update_response = self.render_server.get_job(self.__class__.test_job_id) if update_response: print(f"Status: {update_response['status']}") diff --git a/tests/test_distributed_job_manager.py b/tests/test_distributed_job_manager.py index e9bbf5c..48f5d9c 100644 --- a/tests/test_distributed_job_manager.py +++ b/tests/test_distributed_job_manager.py @@ -127,7 +127,7 @@ class TestFindAvailableServers: mock_get_props.return_value = {'api_version': '0.1', 'system_os': 'macos'} mock_proxy = MagicMock() - mock_proxy.is_engine_available.return_value = { + mock_proxy.get_engine_availability.return_value = { 'available': True, 'hostname': 'server-1.local', } @@ -136,3 +136,4 @@ class TestFindAvailableServers: result = DistributedJobManager.find_available_servers('blender') assert len(result) == 1 assert result[0]['hostname'] == 'server-1.local' + mock_proxy.get_engine_availability.assert_called_once_with('blender')