Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions src/aignostics/application/_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import requests
from loguru import logger

from aignostics.platform import ItemOutput, ItemState, Run, generate_signed_url
from aignostics.platform import ItemOutput, ItemState, OutputArtifactElement, Run, generate_signed_url
from aignostics.utils import sanitize_path_component

from ._models import DownloadProgress, DownloadProgressState
Expand Down Expand Up @@ -146,6 +146,44 @@ def update_progress(
download_progress_queue.put_nowait(progress)


def _resolve_artifact_url(
artifact: OutputArtifactElement,
run_id: str,
get_artifact_download_url: Callable[[str, str], str] | None,
) -> str | None:
"""Resolve the download URL for an artifact.

Tries the new file endpoint first (via get_artifact_download_url), then falls back to
the deprecated download_url field on the artifact if the endpoint fails.

Args:
artifact: The artifact object with optional output_artifact_id and download_url.
run_id: The run ID, passed to get_artifact_download_url.
get_artifact_download_url: Callable for the new endpoint, or None to skip.

Returns:
str | None: The resolved download URL, or None if unavailable.

Raises:
Exception: Re-raises if the new endpoint fails and no fallback download_url exists.
"""
if get_artifact_download_url and artifact.output_artifact_id:
try:
return get_artifact_download_url(run_id, artifact.output_artifact_id)
except Exception as e:
fallback_url: str | None = getattr(artifact, "download_url", None)
if fallback_url:
logger.warning(
"Failed to resolve download URL via file endpoint for artifact {} ({}). "
"Falling back to deprecated download_url field.",
artifact.output_artifact_id,
e,
)
return fallback_url
raise
return getattr(artifact, "download_url", None)


def download_available_items( # noqa: PLR0913, PLR0917
progress: DownloadProgress,
application_run: Run,
Expand All @@ -154,6 +192,7 @@ def download_available_items( # noqa: PLR0913, PLR0917
create_subdirectory_per_item: bool = False,
download_progress_queue: Any | None = None, # noqa: ANN401
download_progress_callable: Callable | None = None, # type: ignore[type-arg]
get_artifact_download_url: Callable[[str, str], str] | None = None,
) -> None:
"""Download items that are available and not yet downloaded.

Expand All @@ -165,6 +204,9 @@ def download_available_items( # noqa: PLR0913, PLR0917
create_subdirectory_per_item (bool): Whether to create a subdirectory for each item.
download_progress_queue (Queue | None): Queue for GUI progress updates.
download_progress_callable (Callable | None): Callback for CLI progress updates.
get_artifact_download_url (Callable[[str, str], str] | None): Callback that takes
(run_id, artifact_id) and returns a presigned download URL. If None, falls back
to artifact.download_url (deprecated).
"""
items = list(application_run.results())
progress.item_count = len(items)
Expand Down Expand Up @@ -201,9 +243,16 @@ def download_available_items( # noqa: PLR0913, PLR0917
progress.artifact = artifact
update_progress(progress, download_progress_callable, download_progress_queue)

# Resolve artifact download URL via the new file endpoint, with fallback
artifact_url = _resolve_artifact_url(artifact, application_run.run_id, get_artifact_download_url)
if not artifact_url:
logger.warning("No download URL available for artifact {}", artifact.output_artifact_id)
continue

download_item_artifact(
progress,
artifact,
artifact_url,
item_directory,
item.external_id if not create_subdirectory_per_item else "",
download_progress_queue,
Expand All @@ -216,6 +265,7 @@ def download_available_items( # noqa: PLR0913, PLR0917
def download_item_artifact( # noqa: PLR0913, PLR0917
progress: DownloadProgress,
artifact: Any, # noqa: ANN401
artifact_download_url: str,
destination_directory: Path,
prefix: str = "",
download_progress_queue: Any | None = None, # noqa: ANN401
Expand All @@ -226,6 +276,7 @@ def download_item_artifact( # noqa: PLR0913, PLR0917
Args:
progress (DownloadProgress): Progress tracking object for GUI or CLI updates.
artifact (Any): The artifact to download.
artifact_download_url (str): The presigned URL to download the artifact from.
destination_directory (Path): Directory to save the file.
prefix (str): Prefix for the file name, if needed.
download_progress_queue (Queue | None): Queue for GUI progress updates.
Expand Down Expand Up @@ -260,7 +311,7 @@ def download_item_artifact( # noqa: PLR0913, PLR0917

download_file_with_progress(
progress,
artifact.download_url,
artifact_download_url,
artifact_path,
metadata_checksum,
download_progress_queue,
Expand Down
112 changes: 87 additions & 25 deletions src/aignostics/application/_gui/_page_application_run_describe.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
ui, # noq
)
from nicegui import run as nicegui_run
from nicegui.events import ClickEventArguments

from aignostics.platform import ItemOutput, ItemResult, ItemState, RunState
from aignostics.platform import ItemOutput, ItemResult, ItemState, Run, RunState
from aignostics.third_party.showinfm.showinfm import show_in_file_manager
from aignostics.utils import GUILocalFilePicker, get_user_data_directory

Expand Down Expand Up @@ -338,9 +339,9 @@ async def start_download() -> None:

# Activate the timer now that download is starting
progress_timer.activate()
download_button.disable()
download_button.props(add="loading")
try:
download_button.disable()
download_button.props(add="loading")
results_folder = await nicegui_run.cpu_bound(
Service.application_run_download_static,
run_id=run.run_id,
Expand All @@ -364,19 +365,24 @@ async def start_download() -> None:
else:
ui.notify("Download completed.", type="positive")
show_in_file_manager(str(results_folder))
except ValueError as e:
except Exception as e:
logger.exception(
"Download failed for run {} (qupath_project={}, marimo={}, folder={})",
run.run_id,
current_qupath_project,
current_marimo,
current_folder,
)
ui.notify(f"Download failed: {e}", type="negative", multi_line=True)
finally:
progress_timer.deactivate()
progress_state["queue"] = None
return
progress_timer.deactivate()
progress_state["queue"] = None
download_button.props(remove="loading")
download_button.enable()
download_item_status.set_visibility(False)
download_item_progress.set_visibility(False)
download_artifact_status.set_visibility(False)
download_artifact_progress.set_visibility(False)
download_button.props(remove="loading")
download_button.enable()
download_item_status.set_visibility(False)
download_item_progress.set_visibility(False)
download_artifact_status.set_visibility(False)
download_artifact_progress.set_visibility(False)

ui.separator()
with ui.row(align_items="end").classes("w-full justify-end"):
Expand Down Expand Up @@ -781,29 +787,85 @@ def render_item(item: ItemResult) -> None: # noqa: C901, PLR0912, PLR0915
icon=mime_type_to_icon(mime_type),
group="artifacts",
).classes("w-full"):
if artifact.download_url:
url = artifact.download_url
if artifact.output_artifact_id:
artifact_id = artifact.output_artifact_id
title = artifact.name
metadata = artifact.metadata

with ui.button_group():
if mime_type == "image/tiff":
ui.button(
preview_button = ui.button(
"Preview",
icon=mime_type_to_icon(mime_type),
on_click=lambda _, url=url, title=title: tiff_dialog_open(title, url),
)

async def _preview_tiff(
_: ClickEventArguments,
aid: str = artifact_id,
t: str = title,
_run: Run = run,
_btn: ui.button = preview_button,
) -> None:
try:
_btn.props(add="loading")
url = await nicegui_run.io_bound(
_run.get_artifact_download_url, aid
)
tiff_dialog_open(t, url)
except Exception as e:
ui.notify(f"Failed to resolve preview URL: {e}", type="warning")
finally:
_btn.props(remove="loading")

preview_button.on_click(_preview_tiff)

if mime_type == "text/csv":
ui.button(
preview_button = ui.button(
"Preview",
icon=mime_type_to_icon(mime_type),
on_click=lambda _, url=url, title=title: csv_dialog_open(title, url),
)
if url:
ui.button(
text="Download",
icon="cloud_download",
on_click=lambda _, url=url: webbrowser.open(url),
)

async def _preview_csv(
_: ClickEventArguments,
aid: str = artifact_id,
t: str = title,
_run: Run = run,
_btn: ui.button = preview_button,
) -> None:
try:
_btn.props(add="loading")
url = await nicegui_run.io_bound(
_run.get_artifact_download_url, aid
)
csv_dialog_open(t, url)
except Exception as e:
ui.notify(f"Failed to resolve preview URL: {e}", type="warning")
finally:
_btn.props(remove="loading")

preview_button.on_click(_preview_csv)

artifact_dl_button = ui.button(
text="Download",
icon="cloud_download",
)

async def _download_artifact(
_: ClickEventArguments,
aid: str = artifact_id,
_run: Run = run,
_btn: ui.button = artifact_dl_button,
) -> None:
try:
_btn.props(add="loading")
url = await nicegui_run.io_bound(_run.get_artifact_download_url, aid)
webbrowser.open(url)
except Exception as e:
ui.notify(f"Failed to resolve download URL: {e}", type="warning")
finally:
_btn.props(remove="loading")

artifact_dl_button.on_click(_download_artifact)
if metadata:
ui.button(
text="Schema",
Expand Down
14 changes: 14 additions & 0 deletions src/aignostics/application/_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,19 @@ def update_qupath_add_input_progress(qupath_add_input_progress: QuPathAddProgres
update_progress(progress, download_progress_callable, download_progress_queue)

downloaded_items: set[str] = set() # Track downloaded items to avoid re-downloading

def _get_artifact_download_url(run_id: str, artifact_id: str) -> str:
"""Resolve artifact download URL via the new API endpoint.

Args:
run_id (str): The run ID.
artifact_id (str): The artifact ID.

Returns:
str: The presigned download URL.
"""
return self._get_platform_client().run(run_id).get_artifact_download_url(artifact_id)

while True:
run_details = application_run.details() # (Re)load current run details
progress.run = run_details
Expand All @@ -1496,6 +1509,7 @@ def update_qupath_add_input_progress(qupath_add_input_progress: QuPathAddProgres
create_subdirectory_per_item,
download_progress_queue,
download_progress_callable,
get_artifact_download_url=_get_artifact_download_url,
)

if run_details.state == RunState.TERMINATED:
Expand Down
3 changes: 2 additions & 1 deletion src/aignostics/platform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
get_mime_type_for_artifact,
mime_type_to_file_ending,
)
from .resources.runs import LIST_APPLICATION_RUNS_MAX_PAGE_SIZE, LIST_APPLICATION_RUNS_MIN_PAGE_SIZE, Run
from .resources.runs import LIST_APPLICATION_RUNS_MAX_PAGE_SIZE, LIST_APPLICATION_RUNS_MIN_PAGE_SIZE, Artifact, Run

__all__ = [
"API_ROOT_DEV",
Expand Down Expand Up @@ -146,6 +146,7 @@
"Application",
"ApplicationSummary",
"ApplicationVersion",
"Artifact",
"Client",
"InputArtifact",
"InputArtifactData",
Expand Down
Loading
Loading