Skip to content

Commit c136907

Browse files
committed
fixup! feat(platform): add 'for_organization' to list all runs of an org
1 parent 0ac055a commit c136907

File tree

4 files changed

+73
-1
lines changed

4 files changed

+73
-1
lines changed

src/aignostics/application/_cli.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
DEFAULT_GPU_TYPE,
2020
DEFAULT_MAX_GPUS_PER_SLIDE,
2121
DEFAULT_NODE_ACQUISITION_TIMEOUT_MINUTES,
22+
ForbiddenException,
2223
NotFoundException,
2324
RunState,
2425
)
@@ -872,6 +873,13 @@ def run_list( # noqa: PLR0913, PLR0917
872873
] = None,
873874
query: Annotated[str | None, typer.Option(help="Optional query string to filter runs by note OR tags.")] = None,
874875
note_case_insensitive: Annotated[bool, typer.Option(help="Make note regex search case-insensitive.")] = True,
876+
for_organization: Annotated[
877+
str | None,
878+
typer.Option(
879+
"--for-organization",
880+
help="Organization ID to list all runs for. Lists runs from all users in the organization.",
881+
),
882+
] = None,
875883
format: Annotated[ # noqa: A002
876884
str,
877885
typer.Option(help="Output format: 'text' (default) or 'json'"),
@@ -885,6 +893,7 @@ def run_list( # noqa: PLR0913, PLR0917
885893
note_regex=note_regex,
886894
note_query_case_insensitive=note_case_insensitive,
887895
query=query,
896+
for_organization=for_organization,
888897
)
889898
if len(runs) == 0:
890899
if format == "json":
@@ -894,6 +903,8 @@ def run_list( # noqa: PLR0913, PLR0917
894903
message = f"You did not yet create a run matching tags: {tags!r}."
895904
elif note_regex:
896905
message = f"You did not yet create a run matching note pattern: {note_regex!r}."
906+
elif for_organization:
907+
message = f"No runs found for organization '{for_organization}'."
897908
else:
898909
message = "You did not yet create a run."
899910
logger.warning(message)
@@ -908,6 +919,11 @@ def run_list( # noqa: PLR0913, PLR0917
908919
message = f"Listed '{len(runs)}' run(s)."
909920
console.print(message, style="info")
910921
logger.debug(f"Listed '{len(runs)}' run(s).")
922+
except ForbiddenException:
923+
message = "Access denied: you are not authorized to list runs."
924+
logger.warning(message)
925+
console.print(f"[error]Error:[/error] {message}")
926+
sys.exit(2)
911927
except Exception as e:
912928
logger.exception("Failed to list runs")
913929
console.print(f"[error]Error:[/error] Failed to list runs: {e}")

src/aignostics/application/_service.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ApplicationSummary,
2424
ApplicationVersion,
2525
Client,
26+
ForbiddenException,
2627
InputArtifact,
2728
InputItem,
2829
NotFoundException,
@@ -533,6 +534,7 @@ def application_runs_static( # noqa: PLR0913, PLR0917
533534
tags: set[str] | None = None,
534535
query: str | None = None,
535536
limit: int | None = None,
537+
for_organization: str | None = None,
536538
) -> list[dict[str, Any]]:
537539
"""Get a list of all application runs, static variant.
538540
@@ -551,6 +553,8 @@ def application_runs_static( # noqa: PLR0913, PLR0917
551553
If None, no filtering is applied. Cannot be used together with custom_metadata, note_regex, or tags.
552554
Performs a union search: matches runs where the query appears in the note OR matches any tag.
553555
limit (int | None): The maximum number of runs to retrieve. If None, all runs are retrieved.
556+
for_organization (str | None): If set, returns all runs triggered by users of the specified
557+
organization. If None, only the runs of the current user are returned.
554558
555559
Returns:
556560
list[RunData]: A list of all application runs.
@@ -587,6 +591,7 @@ def application_runs_static( # noqa: PLR0913, PLR0917
587591
tags=tags,
588592
query=query,
589593
limit=limit,
594+
for_organization=for_organization,
590595
)
591596
]
592597

@@ -601,6 +606,7 @@ def application_runs( # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
601606
tags: set[str] | None = None,
602607
query: str | None = None,
603608
limit: int | None = None,
609+
for_organization: str | None = None,
604610
) -> list[RunData]:
605611
"""Get a list of all application runs.
606612
@@ -619,12 +625,15 @@ def application_runs( # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
619625
If None, no filtering is applied. Cannot be used together with custom_metadata, note_regex, or tags.
620626
Performs a union search: matches runs where the query appears in the note OR matches any tag.
621627
limit (int | None): The maximum number of runs to retrieve. If None, all runs are retrieved.
628+
for_organization (str | None): If set, returns all runs triggered by users of the specified
629+
organization. If None, only the runs of the current user are returned.
622630
623631
Returns:
624632
list[RunData]: A list of all application runs.
625633
626634
Raises:
627635
ValueError: If query is used together with custom_metadata, note_regex, or tags.
636+
ForbiddenException: If the user is not authorized to list runs for the specified organization.
628637
RuntimeError: If the application run list cannot be retrieved.
629638
"""
630639
# Validate that query is not used with other metadata filters
@@ -658,6 +667,7 @@ def application_runs( # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
658667
custom_metadata=custom_metadata_note,
659668
sort="-submitted_at",
660669
page_size=page_size,
670+
for_organization=for_organization,
661671
)
662672
for run in note_run_iterator:
663673
if has_output and run.output == RunOutput.NONE:
@@ -677,6 +687,7 @@ def application_runs( # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
677687
custom_metadata=custom_metadata_tags,
678688
sort="-submitted_at",
679689
page_size=page_size,
690+
for_organization=for_organization,
680691
)
681692
for run in tag_run_iterator:
682693
if has_output and run.output == RunOutput.NONE:
@@ -727,6 +738,7 @@ def application_runs( # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
727738
custom_metadata=custom_metadata,
728739
sort="-submitted_at",
729740
page_size=page_size,
741+
for_organization=for_organization,
730742
)
731743
for run in run_iterator:
732744
if has_output and run.output == RunOutput.NONE:
@@ -766,6 +778,8 @@ def application_runs( # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
766778
if limit is not None and len(runs) >= limit:
767779
break
768780
return runs
781+
except ForbiddenException:
782+
raise
769783
except Exception as e:
770784
message = f"Failed to retrieve application runs: {e}"
771785
logger.exception(message)

src/aignostics/platform/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
Higher level abstractions are provided in the application module.
1111
"""
1212

13-
from aignx.codegen.exceptions import ApiException, NotFoundException
13+
from aignx.codegen.exceptions import ApiException, ForbiddenException, NotFoundException
1414
from aignx.codegen.models import ApplicationReadResponse as Application
1515
from aignx.codegen.models import ApplicationReadShortResponse as ApplicationSummary
1616
from aignx.codegen.models import InputArtifact as InputArtifactData
@@ -147,6 +147,7 @@
147147
"ApplicationSummary",
148148
"ApplicationVersion",
149149
"Client",
150+
"ForbiddenException",
150151
"InputArtifact",
151152
"InputArtifactData",
152153
"InputItem",

tests/aignostics/application/cli_test.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,47 @@ def test_cli_run_list_verbose_limit_1(runner: CliRunner, record_property) -> Non
807807
assert displayed_count == 1, f"Expected listed count to be == 1, but got {displayed_count}"
808808

809809

810+
@pytest.mark.unit
811+
def test_cli_run_list_for_organization(runner: CliRunner) -> None:
812+
"""Check run list command passes --for-organization to service and shows org-specific empty message."""
813+
with patch.object(ApplicationService, "application_runs", return_value=[]) as mock_method:
814+
result = runner.invoke(cli, ["application", "run", "list", "--for-organization", "org-123"])
815+
assert result.exit_code == 0
816+
mock_method.assert_called_once()
817+
assert mock_method.call_args[1]["for_organization"] == "org-123"
818+
output = normalize_output(result.stdout)
819+
assert "No runs found for organization 'org-123'" in output
820+
821+
822+
@pytest.mark.unit
823+
def test_cli_run_list_forbidden_with_organization(runner: CliRunner) -> None:
824+
"""Check ForbiddenException with --for-organization shows org-specific access denied message."""
825+
from aignx.codegen.exceptions import ForbiddenException
826+
827+
with patch.object(
828+
ApplicationService, "application_runs", side_effect=ForbiddenException(status=403, reason="Forbidden")
829+
):
830+
result = runner.invoke(cli, ["application", "run", "list", "--for-organization", "secret-org"])
831+
assert result.exit_code == 2
832+
output = normalize_output(result.stdout)
833+
assert "Access denied" in output
834+
assert "secret-org" in output
835+
836+
837+
@pytest.mark.unit
838+
def test_cli_run_list_forbidden_without_organization(runner: CliRunner) -> None:
839+
"""Check ForbiddenException without --for-organization shows generic access denied message."""
840+
from aignx.codegen.exceptions import ForbiddenException
841+
842+
with patch.object(
843+
ApplicationService, "application_runs", side_effect=ForbiddenException(status=403, reason="Forbidden")
844+
):
845+
result = runner.invoke(cli, ["application", "run", "list"])
846+
assert result.exit_code == 2
847+
output = normalize_output(result.stdout)
848+
assert "Access denied: you are not authorized to list runs." in output
849+
850+
810851
# TODO(Andreas): This previously failed as invalid run id. Is it expected this now calls the API?
811852
@pytest.mark.e2e
812853
@pytest.mark.timeout(timeout=60)

0 commit comments

Comments
 (0)