Skip to content

Commit eee0dd2

Browse files
committed
WIP
1 parent 5b6d89b commit eee0dd2

File tree

5 files changed

+213
-7
lines changed

5 files changed

+213
-7
lines changed

src/aignostics/application/_gui/_page_application_describe.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -794,13 +794,15 @@ def _upload_ui(metadata: list[dict[str, Any]]) -> None:
794794
ui.label(f"Upload and submit your {len(metadata)} slide(s) for analysis.")
795795

796796
# Allow users of some organisations to request onboarding slides to Portal
797+
from aignostics.constants import INTERNAL_ORGS # noqa: PLC0415
798+
797799
user_info: UserInfo | None = app.storage.tab.get("user_info", None)
798800
with ui.row().classes("full-width mt-4 mb-4"):
799801
if (
800802
user_info
801803
and user_info.organization
802804
and user_info.organization.name
803-
and user_info.organization.name.lower() in {"aignostics", "pre-alpha-org", "lmu", "charite"}
805+
and user_info.organization.name.lower() in INTERNAL_ORGS
804806
):
805807
ui.checkbox(
806808
text="Onboard Slides and Output to Aignostics Portal",
@@ -835,12 +837,14 @@ def _update_upload_progress() -> None:
835837
_upload_ui.refresh(submit_form.metadata)
836838

837839
with ui.step("Pipeline"):
840+
from aignostics.constants import INTERNAL_ORGS # noqa: PLC0415
841+
838842
user_info: UserInfo | None = app.storage.tab.get("user_info", None)
839843
can_configure_pipeline = (
840844
user_info
841845
and user_info.organization
842846
and user_info.organization.name
843-
and user_info.organization.name.lower() in {"aignostics", "pre-alpha-org", "lmu", "charite"}
847+
and user_info.organization.name.lower() in INTERNAL_ORGS
844848
)
845849

846850
if can_configure_pipeline:

src/aignostics/application/_gui/_page_application_run_describe.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,37 @@ def open_marimo(results_folder: Path, button: ui.button | None = None) -> None:
510510
ui.navigate.reload() # TODO(Helmut): Find out why this workaround works. Was just a hunch ...
511511

512512
if run_data: # noqa: PLR1702
513+
# Display queue position banner if the run is pending/processing and has queue info
514+
from aignostics.constants import INTERNAL_ORGS # noqa: PLC0415
515+
516+
user_info: UserInfo | None = app.storage.tab.get("user_info", None)
517+
is_internal_user = (
518+
user_info
519+
and user_info.organization
520+
and user_info.organization.name
521+
and user_info.organization.name.lower() in INTERNAL_ORGS
522+
)
523+
524+
# Show queue position for non-terminated runs
525+
if run_data.state in {RunState.PENDING, RunState.PROCESSING}:
526+
org_position = run_data.num_preceding_items_org
527+
platform_position = run_data.num_preceding_items_platform
528+
529+
if org_position is not None or platform_position is not None:
530+
with ui.card().classes("w-full bg-blue-50 mb-4"), ui.row().classes("items-center gap-2"):
531+
ui.icon("queue", color="blue").classes("text-2xl")
532+
if is_internal_user and platform_position is not None:
533+
# Show both org and platform positions for Aignostics users
534+
org_str = str(org_position) if org_position is not None else "N/A"
535+
ui.label(
536+
f"Queue Position: {org_str} items ahead (org), {platform_position} items ahead (platform)"
537+
).classes("text-blue-800 font-medium")
538+
elif org_position is not None:
539+
# Show only org position for external users
540+
ui.label(f"Queue Position: {org_position} items ahead in your organization's queue").classes(
541+
"text-blue-800 font-medium"
542+
)
543+
513544
with ui.row().classes("w-full justify-center"):
514545
expansion = ui.expansion(text=f"Run {run.run_id}", icon="info")
515546
expansion.on_value_change(
@@ -550,7 +581,6 @@ def open_marimo(results_folder: Path, button: ui.button | None = None) -> None:
550581
""",
551582
language="markdown",
552583
).classes("full-width").mark("CODE_RUN_METADATA")
553-
user_info: UserInfo | None = app.storage.tab.get("user_info", None)
554584
if run_data.custom_metadata:
555585
is_editable = user_info and user_info.role in {"admin", "super_admin"}
556586
properties = {

src/aignostics/application/_utils.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,43 @@ def _format_run_statistics(statistics: RunItemStatistics) -> str:
225225
)
226226

227227

228-
def _format_run_details(run: RunData) -> str:
228+
def _format_queue_position(run: RunData, is_aignostics_user: bool = False) -> str:
229+
"""Format queue position information for a run.
230+
231+
Shows the number of preceding items in the queue. For Aignostics users,
232+
shows both organization-level and platform-level queue positions.
233+
For other users, shows only the organization-level position.
234+
235+
Args:
236+
run (RunData): Run data containing queue position info
237+
is_aignostics_user (bool): Whether the user is from Aignostics org
238+
239+
Returns:
240+
str: Formatted queue position string, or empty string if no queue info available
241+
"""
242+
org_position = run.num_preceding_items_org
243+
platform_position = run.num_preceding_items_platform
244+
245+
if org_position is None and platform_position is None:
246+
return ""
247+
248+
if is_aignostics_user:
249+
# Show both org and platform positions for Aignostics users
250+
org_str = str(org_position) if org_position is not None else "N/A"
251+
platform_str = str(platform_position) if platform_position is not None else "N/A"
252+
return f"[bold]Queue Position:[/bold] {org_str} items ahead (org), {platform_str} items ahead (platform)\n"
253+
# Show only org position for external users
254+
if org_position is not None:
255+
return f"[bold]Queue Position:[/bold] {org_position} items ahead in your organization's queue\n"
256+
return ""
257+
258+
259+
def _format_run_details(run: RunData, is_aignostics_user: bool = False) -> str:
229260
"""Format detailed run information as a single string.
230261
231262
Args:
232263
run (RunData): Run data to format
264+
is_aignostics_user (bool): Whether the user is from Aignostics org (shows additional queue info)
233265
234266
Returns:
235267
str: Formatted run details
@@ -244,6 +276,11 @@ def _format_run_details(run: RunData) -> str:
244276
f"[bold]Output:[/bold] {run.output.value}\n"
245277
)
246278

279+
# Add queue position info if available
280+
queue_position_str = _format_queue_position(run, is_aignostics_user)
281+
if queue_position_str:
282+
output += queue_position_str
283+
247284
if run.error_message or run.error_code:
248285
output += f"[bold]Error Message (Code):[/bold] {run.error_message or 'N/A'} ({run.error_code or 'N/A'})\n"
249286

@@ -258,6 +295,24 @@ def _format_run_details(run: RunData) -> str:
258295
return output
259296

260297

298+
def _is_internal_user() -> bool:
299+
"""Check if the current user is from an internal organization.
300+
301+
Returns:
302+
bool: True if user is from an internal org, False otherwise
303+
"""
304+
try:
305+
from aignostics.constants import INTERNAL_ORGS # noqa: PLC0415
306+
from aignostics.platform import Client # noqa: PLC0415
307+
308+
me = Client().me()
309+
if me and me.organization and me.organization.name:
310+
return me.organization.name.lower() in INTERNAL_ORGS
311+
except Exception:
312+
logger.debug("Could not determine user organization for queue position display")
313+
return False
314+
315+
261316
def retrieve_and_print_run_details(run_handle: Run) -> None:
262317
"""Retrieve and print detailed information about a run.
263318
@@ -266,8 +321,10 @@ def retrieve_and_print_run_details(run_handle: Run) -> None:
266321
267322
"""
268323
run = run_handle.details()
324+
is_internal = _is_internal_user()
269325

270-
output = f"[bold]Run Details for {run.run_id}[/bold]\n{'=' * 80}\n{_format_run_details(run)}\n\n[bold]Items:[/bold]"
326+
run_details = _format_run_details(run, is_internal)
327+
output = f"[bold]Run Details for {run.run_id}[/bold]\n{'=' * 80}\n{run_details}\n\n[bold]Items:[/bold]"
271328

272329
console.print(output)
273330
_retrieve_and_print_run_items(run_handle)

src/aignostics/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@
3737
TEST_APP_APPLICATION_ID = "test-app"
3838
WSI_SUPPORTED_FILE_EXTENSIONS = {".dcm", ".tiff", ".tif", ".svs"}
3939
WSI_SUPPORTED_FILE_EXTENSIONS_TEST_APP = {".tiff"}
40+
41+
# Organizations with internal/advanced access (e.g., platform-wide queue visibility, GPU config)
42+
INTERNAL_ORGS = {"aignostics", "pre-alpha-org", "lmu", "charite"}

tests/aignostics/application/utils_test.py

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,9 +452,120 @@ def test_print_runs_non_verbose_with_error(mock_console: Mock) -> None:
452452
assert "USER_CANCELED" in call_args
453453

454454

455+
# Tests for _format_queue_position
456+
457+
458+
@pytest.mark.unit
459+
def test_format_queue_position_no_data() -> None:
460+
"""Test queue position formatting when no queue data is available."""
461+
from aignostics.application._utils import _format_queue_position
462+
463+
run_data = RunData(
464+
run_id="run-123",
465+
application_id="he-tme",
466+
version_number="1.0.0",
467+
state=RunState.PENDING,
468+
termination_reason=None,
469+
output=RunOutput.NONE,
470+
statistics=RunItemStatistics(
471+
item_count=1,
472+
item_pending_count=1,
473+
item_processing_count=0,
474+
item_skipped_count=0,
475+
item_succeeded_count=0,
476+
item_user_error_count=0,
477+
item_system_error_count=0,
478+
),
479+
submitted_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC),
480+
submitted_by="user@example.com",
481+
terminated_at=None,
482+
custom_metadata=None,
483+
error_message=None,
484+
error_code=None,
485+
num_preceding_items_org=None,
486+
num_preceding_items_platform=None,
487+
)
488+
489+
result = _format_queue_position(run_data, is_aignostics_user=False)
490+
assert not result
491+
492+
493+
@pytest.mark.unit
494+
def test_format_queue_position_external_user() -> None:
495+
"""Test queue position formatting for external users (org only)."""
496+
from aignostics.application._utils import _format_queue_position
497+
498+
run_data = RunData(
499+
run_id="run-123",
500+
application_id="he-tme",
501+
version_number="1.0.0",
502+
state=RunState.PENDING,
503+
termination_reason=None,
504+
output=RunOutput.NONE,
505+
statistics=RunItemStatistics(
506+
item_count=1,
507+
item_pending_count=1,
508+
item_processing_count=0,
509+
item_skipped_count=0,
510+
item_succeeded_count=0,
511+
item_user_error_count=0,
512+
item_system_error_count=0,
513+
),
514+
submitted_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC),
515+
submitted_by="user@example.com",
516+
terminated_at=None,
517+
custom_metadata=None,
518+
error_message=None,
519+
error_code=None,
520+
num_preceding_items_org=5,
521+
num_preceding_items_platform=100,
522+
)
523+
524+
result = _format_queue_position(run_data, is_aignostics_user=False)
525+
assert "5 items ahead in your organization's queue" in result
526+
assert "platform" not in result.lower()
527+
528+
529+
@pytest.mark.unit
530+
def test_format_queue_position_aignostics_user() -> None:
531+
"""Test queue position formatting for Aignostics users (both org and platform)."""
532+
from aignostics.application._utils import _format_queue_position
533+
534+
run_data = RunData(
535+
run_id="run-123",
536+
application_id="he-tme",
537+
version_number="1.0.0",
538+
state=RunState.PENDING,
539+
termination_reason=None,
540+
output=RunOutput.NONE,
541+
statistics=RunItemStatistics(
542+
item_count=1,
543+
item_pending_count=1,
544+
item_processing_count=0,
545+
item_skipped_count=0,
546+
item_succeeded_count=0,
547+
item_user_error_count=0,
548+
item_system_error_count=0,
549+
),
550+
submitted_at=datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC),
551+
submitted_by="user@example.com",
552+
terminated_at=None,
553+
custom_metadata=None,
554+
error_message=None,
555+
error_code=None,
556+
num_preceding_items_org=5,
557+
num_preceding_items_platform=100,
558+
)
559+
560+
result = _format_queue_position(run_data, is_aignostics_user=True)
561+
assert "5 items ahead (org)" in result
562+
assert "100 items ahead (platform)" in result
563+
564+
455565
@pytest.mark.unit
566+
@patch("aignostics.application._utils._is_internal_user", return_value=False)
456567
@patch("aignostics.application._utils.console")
457-
def test_retrieve_and_print_run_details_with_items(mock_console: Mock) -> None:
568+
def test_retrieve_and_print_run_details_with_items(mock_console: Mock, mock_is_internal: Mock) -> None:
458569
"""Test retrieving and printing run details with items."""
459570
submitted_at = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
460571
terminated_at = datetime(2025, 1, 1, 13, 0, 0, tzinfo=UTC)
@@ -530,8 +641,9 @@ def test_retrieve_and_print_run_details_with_items(mock_console: Mock) -> None:
530641

531642

532643
@pytest.mark.unit
644+
@patch("aignostics.application._utils._is_internal_user", return_value=False)
533645
@patch("aignostics.application._utils.console")
534-
def test_retrieve_and_print_run_details_no_items(mock_console: Mock) -> None:
646+
def test_retrieve_and_print_run_details_no_items(mock_console: Mock, mock_is_internal: Mock) -> None:
535647
"""Test retrieving and printing run details with no items."""
536648
submitted_at = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
537649

0 commit comments

Comments
 (0)