Jobs API cleanup

This commit is contained in:
Brett Williams
2026-06-06 08:01:48 -05:00
parent d49cd9df79
commit 5e154b6bab
6 changed files with 53 additions and 52 deletions
+3 -3
View File
@@ -109,9 +109,9 @@ curl -X POST http://localhost:8080/api/add_job \
- **UI**: View job status, progress, logs, and worker availability in real-time. - **UI**: View job status, progress, logs, and worker availability in real-time.
- **API Endpoints**: - **API Endpoints**:
- `GET /api/jobs`: List all jobs - `GET /api/jobs`: List all jobs
- `GET /api/job/<job_id>`: Get job details - `GET /api/jobs/<job_id>`: Get job details
- `POST /api/job/<job_id>/cancel`: Cancel a job - `POST /api/jobs/<job_id>/cancel`: Cancel a job
- `POST /api/job/<job_id>/delete`: Delete a job - `POST /api/jobs/<job_id>/delete`: Delete a job
- `GET /api/status`: Get server and queue status - `GET /api/status`: Get server and queue status
- `GET /api/engines`: List engine information - `GET /api/engines`: List engine information
+12 -12
View File
@@ -48,7 +48,7 @@ Known callers:
- `RenderServerProxy.get_all_jobs()` - `RenderServerProxy.get_all_jobs()`
- `src/ui/main_window.py` - `src/ui/main_window.py`
### `GET /api/jobs_long_poll` ### `GET /api/jobs/long_poll`
Long-polls the job list until the supplied cache token changes or 30 seconds Long-polls the job list until the supplied cache token changes or 30 seconds
elapse. elapse.
@@ -68,7 +68,7 @@ Known callers:
- `RenderServerProxy.get_all_jobs()` through the background cache updater. - `RenderServerProxy.get_all_jobs()` through the background cache updater.
### `GET /api/jobs/<status_val>` ### `GET /api/jobs/status/<status_val>`
Returns jobs matching a render status. Returns jobs matching a render status.
@@ -86,7 +86,7 @@ Responses:
Review note: this route is not currently wrapped by `RenderServerProxy` and no Review note: this route is not currently wrapped by `RenderServerProxy` and no
in-repo callers were found. in-repo callers were found.
### `GET /api/job/<job_id>` ### `GET /api/jobs/<job_id>`
Returns one job as JSON. Returns one job as JSON.
@@ -98,7 +98,7 @@ Known callers:
- `src/distributed_job_manager.py` - `src/distributed_job_manager.py`
- `tests/job_creation_tests.py` - `tests/job_creation_tests.py`
### `GET /api/job/<job_id>/logs` ### `GET /api/jobs/<job_id>/logs`
Returns the job log file as `text/plain`. Returns the job log file as `text/plain`.
@@ -106,7 +106,7 @@ Known callers:
- `src/ui/main_window.py` opens this URL directly. - `src/ui/main_window.py` opens this URL directly.
### `GET /api/job/<job_id>/file_list` ### `GET /api/jobs/<job_id>/files`
Returns a list of output filenames for the job. Returns a list of output filenames for the job.
@@ -115,7 +115,7 @@ Known callers:
- `RenderServerProxy.get_job_files_list()` - `RenderServerProxy.get_job_files_list()`
- `src/utilities/server_helper.py` - `src/utilities/server_helper.py`
### `GET /api/job/<job_id>/download` ### `GET /api/jobs/<job_id>/download`
Downloads one output file. Downloads one output file.
@@ -136,7 +136,7 @@ Known callers:
- `RenderServerProxy.download_job_file()` - `RenderServerProxy.download_job_file()`
- `src/utilities/server_helper.py` - `src/utilities/server_helper.py`
### `GET /api/job/<job_id>/download_all` ### `GET /api/jobs/<job_id>/download_all`
Creates a temporary zip of the job output directory and downloads it. Creates a temporary zip of the job output directory and downloads it.
@@ -146,7 +146,7 @@ Known callers:
- `src/ui/main_window.py` - `src/ui/main_window.py`
- `src/utilities/server_helper.py` - `src/utilities/server_helper.py`
### `GET /api/job/<job_id>/thumbnail` ### `GET /api/jobs/<job_id>/thumbnail`
Returns a generated preview image or video for a job. Returns a generated preview image or video for a job.
@@ -207,7 +207,7 @@ Known callers:
- `src/distributed_job_manager.py` - `src/distributed_job_manager.py`
- `tests/job_creation_tests.py` - `tests/job_creation_tests.py`
### `POST /api/job/<job_id>/cancel` ### `POST /api/jobs/<job_id>/cancel`
Cancels a job. Cancels a job.
@@ -224,7 +224,7 @@ Known callers:
- `src/ui/main_window.py` - `src/ui/main_window.py`
- `src/distributed_job_manager.py` - `src/distributed_job_manager.py`
### `POST /api/job/<job_id>/delete` ### `POST /api/jobs/<job_id>/delete`
Deletes a job, stops it first, deletes previews, and removes owned upload/output Deletes a job, stops it first, deletes previews, and removes owned upload/output
directories when safe. directories when safe.
@@ -240,7 +240,7 @@ Known callers:
- `RenderServerProxy.delete_job()` - `RenderServerProxy.delete_job()`
- `src/ui/main_window.py` - `src/ui/main_window.py`
### `POST /api/job/<job_id>/send_subjob_update_notification` ### `POST /api/jobs/<job_id>/subjob_update`
Notifies a parent job that a child/subjob changed state. Notifies a parent job that a child/subjob changed state.
@@ -498,7 +498,7 @@ Likely redundant or overlapping routes:
used internally by `/api/full_status`. used internally by `/api/full_status`.
Routes with no in-repo callers found: Routes with no in-repo callers found:
- `GET /api/jobs/<status_val>` - `GET /api/jobs/status/<status_val>`
- `GET /api/presets` - `GET /api/presets`
- `GET /api/disk_benchmark` - `GET /api/disk_benchmark`
- `GET /api/engines/<engine_name>/args` - `GET /api/engines/<engine_name>/args`
+12 -12
View File
@@ -288,7 +288,7 @@
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs_long_poll</span></div> <div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/long_poll</span></div>
<p>Long-polls the job list until the supplied cache token changes or 30 seconds elapse.</p> <p>Long-polls the job list until the supplied cache token changes or 30 seconds elapse.</p>
<table> <table>
<tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr> <tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr>
@@ -298,30 +298,30 @@
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/&lt;status_val&gt;</span></div> <div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/status/&lt;status_val&gt;</span></div>
<p>Returns jobs matching a render status converted by <code>string_to_status()</code>.</p> <p>Returns jobs matching a render status converted by <code>string_to_status()</code>.</p>
<div class="note">No <code>RenderServerProxy</code> wrapper or in-repo caller was found.</div> <div class="note">No <code>RenderServerProxy</code> wrapper or in-repo caller was found.</div>
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/job/&lt;job_id&gt;</span></div> <div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/&lt;job_id&gt;</span></div>
<p>Returns one job as JSON.</p> <p>Returns one job as JSON.</p>
<p>Known callers include <code>RenderServerProxy.get_job_info()</code>, <code>add_job.py</code>, <code>src/ui/main_window.py</code>, <code>src/distributed_job_manager.py</code>, and <code>tests/job_creation_tests.py</code>.</p> <p>Known callers include <code>RenderServerProxy.get_job_info()</code>, <code>add_job.py</code>, <code>src/ui/main_window.py</code>, <code>src/distributed_job_manager.py</code>, and <code>tests/job_creation_tests.py</code>.</p>
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/job/&lt;job_id&gt;/logs</span></div> <div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/&lt;job_id&gt;/logs</span></div>
<p>Returns the job log file as <code>text/plain</code>. The main window opens this URL directly.</p> <p>Returns the job log file as <code>text/plain</code>. The main window opens this URL directly.</p>
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/job/&lt;job_id&gt;/file_list</span></div> <div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/&lt;job_id&gt;/files</span></div>
<p>Returns a list of output filenames for the job.</p> <p>Returns a list of output filenames for the job.</p>
<p>Known callers: <code>RenderServerProxy.get_job_files_list()</code> and <code>src/utilities/server_helper.py</code>.</p> <p>Known callers: <code>RenderServerProxy.get_job_files_list()</code> and <code>src/utilities/server_helper.py</code>.</p>
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/job/&lt;job_id&gt;/download</span></div> <div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/&lt;job_id&gt;/download</span></div>
<p>Downloads one output file.</p> <p>Downloads one output file.</p>
<table> <table>
<tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr> <tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr>
@@ -331,13 +331,13 @@
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/job/&lt;job_id&gt;/download_all</span></div> <div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/&lt;job_id&gt;/download_all</span></div>
<p>Creates a temporary zip of the job output directory and downloads it.</p> <p>Creates a temporary zip of the job output directory and downloads it.</p>
<p>Known callers: <code>RenderServerProxy.download_all_job_files()</code>, <code>src/ui/main_window.py</code>, and <code>src/utilities/server_helper.py</code>.</p> <p>Known callers: <code>RenderServerProxy.download_all_job_files()</code>, <code>src/ui/main_window.py</code>, and <code>src/utilities/server_helper.py</code>.</p>
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/job/&lt;job_id&gt;/thumbnail</span></div> <div class="endpoint-header"><span class="method get">GET</span><span class="path">/api/jobs/&lt;job_id&gt;/thumbnail</span></div>
<p>Returns a generated preview image or video for a job.</p> <p>Returns a generated preview image or video for a job.</p>
<table> <table>
<tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr> <tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr>
@@ -369,7 +369,7 @@
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method post">POST</span><span class="path">/api/job/&lt;job_id&gt;/cancel</span></div> <div class="endpoint-header"><span class="method post">POST</span><span class="path">/api/jobs/&lt;job_id&gt;/cancel</span></div>
<p>Cancels a job. Requires a truthy <code>confirm</code> query parameter.</p> <p>Cancels a job. Requires a truthy <code>confirm</code> query parameter.</p>
<table> <table>
<tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr> <tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr>
@@ -379,7 +379,7 @@
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method post">POST</span><span class="path">/api/job/&lt;job_id&gt;/delete</span></div> <div class="endpoint-header"><span class="method post">POST</span><span class="path">/api/jobs/&lt;job_id&gt;/delete</span></div>
<p>Deletes a job, stops it first, deletes previews, and removes owned upload/output directories when safe.</p> <p>Deletes a job, stops it first, deletes previews, and removes owned upload/output directories when safe.</p>
<table> <table>
<tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr> <tr><th>Query Parameter</th><th>Required</th><th>Description</th></tr>
@@ -388,7 +388,7 @@
</article> </article>
<article class="endpoint"> <article class="endpoint">
<div class="endpoint-header"><span class="method post">POST</span><span class="path">/api/job/&lt;job_id&gt;/send_subjob_update_notification</span></div> <div class="endpoint-header"><span class="method post">POST</span><span class="path">/api/jobs/&lt;job_id&gt;/subjob_update</span></div>
<p>Notifies a parent job that a child/subjob changed state. The request body is the JSON representation of the subjob.</p> <p>Notifies a parent job that a child/subjob changed state. The request body is the JSON representation of the subjob.</p>
</article> </article>
</section> </section>
@@ -571,7 +571,7 @@
<div class="panel" style="margin-top: 14px;"> <div class="panel" style="margin-top: 14px;">
<h3>Routes With No In-Repo Callers Found</h3> <h3>Routes With No In-Repo Callers Found</h3>
<ul> <ul>
<li><code>GET /api/jobs/&lt;status_val&gt;</code></li> <li><code>GET /api/jobs/status/&lt;status_val&gt;</code></li>
<li><code>GET /api/presets</code></li> <li><code>GET /api/presets</code></li>
<li><code>GET /api/disk_benchmark</code></li> <li><code>GET /api/disk_benchmark</code></li>
<li><code>GET /api/engines/&lt;engine_name&gt;/args</code></li> <li><code>GET /api/engines/&lt;engine_name&gt;/args</code></li>
+12 -12
View File
@@ -84,7 +84,7 @@ def jobs_json() -> Dict[str, Any]:
return {'jobs': all_jobs, 'token': job_cache_token} return {'jobs': all_jobs, 'token': job_cache_token}
@server.get('/api/jobs_long_poll') @server.get('/api/jobs/long_poll')
def long_polling_jobs(): def long_polling_jobs():
hash_token = request.args.get('token', None) hash_token = request.args.get('token', None)
start_time = time.time() start_time = time.time()
@@ -98,7 +98,7 @@ def long_polling_jobs():
time.sleep(1) time.sleep(1)
@server.get('/api/jobs/<status_val>') @server.get('/api/jobs/status/<status_val>')
def filtered_jobs_json(status_val): def filtered_jobs_json(status_val):
state = string_to_status(status_val) state = string_to_status(status_val)
jobs = [x.json() for x in RenderQueue.jobs_with_status(state)] jobs = [x.json() for x in RenderQueue.jobs_with_status(state)]
@@ -112,7 +112,7 @@ def filtered_jobs_json(status_val):
# Job Details / File Handling # Job Details / File Handling
# -------------------------------------------- # --------------------------------------------
@server.get('/api/job/<job_id>') @server.get('/api/jobs/<job_id>')
def get_job_details(job_id): def get_job_details(job_id):
"""Retrieves the details of a requested job in JSON format """Retrieves the details of a requested job in JSON format
@@ -125,7 +125,7 @@ def get_job_details(job_id):
return RenderQueue.job_with_id(job_id).json() return RenderQueue.job_with_id(job_id).json()
@server.get('/api/job/<job_id>/logs') @server.get('/api/jobs/<job_id>/logs')
def get_job_logs(job_id): def get_job_logs(job_id):
"""Retrieves the log file for a specific render job. """Retrieves the log file for a specific render job.
@@ -144,12 +144,12 @@ def get_job_logs(job_id):
return Response(log_data, mimetype='text/plain') return Response(log_data, mimetype='text/plain')
@server.get('/api/job/<job_id>/file_list') @server.get('/api/jobs/<job_id>/files')
def get_file_list(job_id): def get_job_files(job_id):
return [Path(p).name for p in RenderQueue.job_with_id(job_id).file_list()] return [Path(p).name for p in RenderQueue.job_with_id(job_id).file_list()]
@server.route('/api/job/<job_id>/download') @server.route('/api/jobs/<job_id>/download')
def download_requested_file(job_id): def download_requested_file(job_id):
requested_filename = request.args.get("filename") requested_filename = request.args.get("filename")
if not requested_filename: if not requested_filename:
@@ -165,7 +165,7 @@ def download_requested_file(job_id):
return f"File '{requested_filename}' not found", 404 return f"File '{requested_filename}' not found", 404
@server.route('/api/job/<job_id>/download_all') @server.route('/api/jobs/<job_id>/download_all')
def download_all_files(job_id): def download_all_files(job_id):
zip_filename = None zip_filename = None
@@ -307,7 +307,7 @@ def add_job_handler():
return 'unknown error', 500 return 'unknown error', 500
@server.post('/api/job/<job_id>/cancel') @server.post('/api/jobs/<job_id>/cancel')
def cancel_job(job_id): def cancel_job(job_id):
if not request.args.get('confirm', False): if not request.args.get('confirm', False):
return 'Confirmation required to cancel job', 400 return 'Confirmation required to cancel job', 400
@@ -321,7 +321,7 @@ def cancel_job(job_id):
return "Unknown error", 500 return "Unknown error", 500
@server.post('/api/job/<job_id>/delete') @server.post('/api/jobs/<job_id>/delete')
def delete_job(job_id): def delete_job(job_id):
try: try:
if not request.args.get("confirm", False): if not request.args.get("confirm", False):
@@ -543,14 +543,14 @@ def delete_engine_download():
def heartbeat(): def heartbeat():
return datetime.now().isoformat(), 200 return datetime.now().isoformat(), 200
@server.post('/api/job/<job_id>/send_subjob_update_notification') @server.post('/api/jobs/<job_id>/subjob_update')
def subjob_update_notification(job_id): def subjob_update_notification(job_id):
subjob_details = request.json subjob_details = request.json
DistributedJobManager.handle_subjob_update_notification(RenderQueue.job_with_id(job_id), subjob_data=subjob_details) DistributedJobManager.handle_subjob_update_notification(RenderQueue.job_with_id(job_id), subjob_data=subjob_details)
return Response(status=200) return Response(status=200)
@server.route('/api/job/<job_id>/thumbnail') @server.route('/api/jobs/<job_id>/thumbnail')
def job_thumbnail(job_id): def job_thumbnail(job_id):
try: try:
+8 -8
View File
@@ -139,7 +139,7 @@ class RenderServerProxy:
if self.__offline_flags: # if we're offline, don't bother with the long poll if self.__offline_flags: # if we're offline, don't bother with the long poll
ignore_token = True 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' not ignore_token) else 'jobs'
status_result = self.request_data(url, timeout=timeout) status_result = self.request_data(url, timeout=timeout)
if status_result is not None: if status_result is not None:
@@ -181,10 +181,10 @@ class RenderServerProxy:
# -------------------------------------------- # --------------------------------------------
def get_job_info(self, job_id, timeout=5): def get_job_info(self, job_id, timeout=5):
return self.request_data(f'job/{job_id}', timeout=timeout) return self.request_data(f'jobs/{job_id}', timeout=timeout)
def get_job_files_list(self, job_id): def get_job_files_list(self, job_id):
return self.request_data(f"job/{job_id}/file_list") return self.request_data(f'jobs/{job_id}/files')
# -------------------------------------------- # --------------------------------------------
# Job Lifecycle: # Job Lifecycle:
@@ -230,10 +230,10 @@ class RenderServerProxy:
return response return response
def cancel_job(self, job_id, confirm=False): def cancel_job(self, job_id, confirm=False):
return self._post(f'job/{job_id}/cancel', params={'confirm': confirm}) return self._post(f'jobs/{job_id}/cancel', params={'confirm': confirm})
def delete_job(self, job_id, confirm=False): def delete_job(self, job_id, confirm=False):
return self._post(f'job/{job_id}/delete', params={'confirm': confirm}) return self._post(f'jobs/{job_id}/delete', params={'confirm': confirm})
def send_subjob_update_notification(self, parent_id, subjob): def send_subjob_update_notification(self, parent_id, subjob):
""" """
@@ -246,7 +246,7 @@ class RenderServerProxy:
Returns: Returns:
Response: The response from the server. 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()) json=subjob.json())
# -------------------------------------------- # --------------------------------------------
@@ -312,11 +312,11 @@ class RenderServerProxy:
# -------------------------------------------- # --------------------------------------------
def download_all_job_files(self, job_id, save_path): 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) return self.__download_file_from_url(url, output_filepath=save_path)
def download_job_file(self, job_id, job_filename, 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) return self.__download_file_from_url(url, output_filepath=save_path)
@staticmethod @staticmethod
+5 -4
View File
@@ -334,7 +334,7 @@ class MainWindow(QMainWindow):
default_image_path = "error.png" default_image_path = "error.png"
before_fetch_hostname = self.current_server_proxy.hostname 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: if response.ok:
try: try:
with io.BytesIO(response.content) as image_data_stream: with io.BytesIO(response.content) as image_data_stream:
@@ -547,7 +547,8 @@ class MainWindow(QMainWindow):
""" """
selected_job_ids = self.selected_job_ids() selected_job_ids = self.selected_job_ids()
if 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 = LogViewer(url)
self.log_viewer_window.show() self.log_viewer_window.show()
@@ -616,8 +617,8 @@ class MainWindow(QMainWindow):
return return
import webbrowser import webbrowser
download_url = (f"http://{self.current_server_proxy.hostname}:{self.current_server_proxy.port}" download_url = (f'http://{self.current_server_proxy.hostname}:{self.current_server_proxy.port}'
f"/api/job/{job_ids[0]}/download_all") f'/api/jobs/{job_ids[0]}/download_all')
webbrowser.open(download_url) webbrowser.open(download_url)
def open_files(self, event): def open_files(self, event):