Skip to content
Open
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
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ FROM builder AS builder-slim
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-build --no-install-project --no-dev --no-editable
uv sync --frozen --no-install-project --no-dev --no-editable

# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
Expand Down Expand Up @@ -59,7 +59,7 @@ FROM builder AS builder-all
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-build --no-install-project --all-extras --no-dev --no-editable
uv sync --frozen --no-install-project --all-extras --no-dev --no-editable

# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
Expand Down
13 changes: 12 additions & 1 deletion specifications/SPEC_PLATFORM_SERVICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,9 +462,20 @@ class ApplicationRun:
self,
nocache: bool = False,
item_ids: list[str] | None = None,
*,
external_ids: list[str] | None = None,
state: ItemState | None = None,
termination_reason: ItemTerminationReason | None = None,
custom_metadata: str | None = None,
) -> Iterator[ItemResultData]:
"""Retrieves the results of items in the run, optionally filtered by item or external IDs."""
"""Retrieves the results of items in the run, optionally filtered server-side.

Filters: ``item_ids`` / ``external_ids`` (identifier-based), ``state`` /
``termination_reason`` (lifecycle), and ``custom_metadata`` (JSONPath
expression evaluated against the item's custom metadata). All filters
are forwarded to ``GET /v1/runs/{run_id}/items``; ``None`` parameters
are omitted from the request.
"""

def item_status(self) -> dict[str, ItemStatus]:
"""Retrieves the status of all items in the run."""
Expand Down
2 changes: 1 addition & 1 deletion src/aignostics/platform/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ def delete(self) -> None:
- ✅ `Applications.list()` - Application list (5 min TTL)
- ✅ `Applications.details()` - Application details (5 min TTL)
- ✅ `Runs.details()` - Run details (15 sec TTL)
- ✅ `Runs.results()` - Run results (15 sec TTL), supports `item_ids` and `external_ids` filters
- ✅ `Runs.results()` - Run results (15 sec TTL), supports `item_ids`, `external_ids`, `state`, `termination_reason`, and `custom_metadata` filters
- ✅ `Runs.list()` - Run list (15 sec TTL)

**Cache Bypass (NEW):**
Expand Down
18 changes: 17 additions & 1 deletion src/aignostics/platform/resources/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
ItemOutput,
ItemResultReadResponse,
ItemState,
ItemTerminationReason,
RunCreationRequest,
RunCreationResponse,
RunState,
Expand Down Expand Up @@ -375,11 +376,15 @@ def delete(self) -> None:
)
operation_cache_clear() # Clear all caches since we added a new run

def results(
def results( # noqa: PLR0913
self,
nocache: bool = False,
item_ids: list[str] | None = None,
external_ids: list[str] | None = None,
*,
state: ItemState | None = None,
termination_reason: ItemTerminationReason | None = None,
custom_metadata: str | None = None,
) -> t.Iterator[ItemResultData]:
"""Retrieves the results of all items in the run.

Expand All @@ -390,6 +395,11 @@ def results(
The fresh result will still be cached for subsequent calls. Defaults to False.
item_ids (list[str] | None): Optional list of item IDs to filter results by.
external_ids (list[str] | None): Optional list of external IDs to filter results by.
state (ItemState | None): Optional filter by item state (server-side).
termination_reason (ItemTerminationReason | None): Optional filter by termination reason
(server-side, only applies to TERMINATED items).
custom_metadata (str | None): Optional JSONPath expression to filter items by their
custom_metadata (server-side).

Returns:
Iterator[ItemResultData]: An iterator over item results.
Expand Down Expand Up @@ -422,6 +432,12 @@ def results_with_retry(run_id: str, **kwargs: object) -> list[ItemResultData]:
filter_kwargs["item_id__in"] = item_ids
if external_ids:
filter_kwargs["external_id__in"] = external_ids
if state is not None:
filter_kwargs["state"] = state
if termination_reason is not None:
filter_kwargs["termination_reason"] = termination_reason
if custom_metadata is not None:
filter_kwargs["custom_metadata"] = custom_metadata

return paginate(lambda **kwargs: results_with_retry(self.run_id, nocache=nocache, **filter_kwargs, **kwargs))

Expand Down
76 changes: 76 additions & 0 deletions tests/aignostics/platform/resources/runs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,3 +1243,79 @@ def test_ensure_artifacts_downloaded_is_instance_method_not_static(app_run, tmp_
bound = app_run.ensure_artifacts_downloaded
# On a method, __self__ is the instance; on a staticmethod, __self__ doesn't exist.
assert getattr(bound, "__self__", None) is app_run


# ---------------------------------------------------------------------------
# Run.results() — server-side state / termination_reason / custom_metadata filters
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_results_passes_state_filter(app_run, mock_api) -> None:
"""state= is forwarded to the API call on every page."""
from aignx.codegen.models import ItemState

Comment on lines +1254 to +1257
mock_api.list_run_items_v1_runs_run_id_items_get.return_value = []

list(app_run.results(state=ItemState.TERMINATED))

call_kwargs = mock_api.list_run_items_v1_runs_run_id_items_get.call_args[1]
assert call_kwargs["state"] == ItemState.TERMINATED

Comment on lines +1254 to +1264

@pytest.mark.unit
def test_results_passes_termination_reason_filter(app_run, mock_api) -> None:
"""termination_reason= is forwarded to the API call on every page."""
from aignx.codegen.models import ItemTerminationReason

mock_api.list_run_items_v1_runs_run_id_items_get.return_value = []

list(app_run.results(termination_reason=ItemTerminationReason.SUCCEEDED))

call_kwargs = mock_api.list_run_items_v1_runs_run_id_items_get.call_args[1]
assert call_kwargs["termination_reason"] == ItemTerminationReason.SUCCEEDED


@pytest.mark.unit
def test_results_passes_custom_metadata_filter(app_run, mock_api) -> None:
"""custom_metadata= is forwarded to the API call on every page."""
mock_api.list_run_items_v1_runs_run_id_items_get.return_value = []

list(app_run.results(custom_metadata="$.key"))

call_kwargs = mock_api.list_run_items_v1_runs_run_id_items_get.call_args[1]
assert call_kwargs["custom_metadata"] == "$.key"


@pytest.mark.unit
def test_results_combines_all_filters(app_run, mock_api) -> None:
"""All three new filters are forwarded together when all are provided."""
from aignx.codegen.models import ItemState, ItemTerminationReason

mock_api.list_run_items_v1_runs_run_id_items_get.return_value = []

list(
app_run.results(
state=ItemState.TERMINATED,
termination_reason=ItemTerminationReason.SUCCEEDED,
custom_metadata="$.batch_id=='x'",
)
)

call_kwargs = mock_api.list_run_items_v1_runs_run_id_items_get.call_args[1]
assert call_kwargs["state"] == ItemState.TERMINATED
assert call_kwargs["termination_reason"] == ItemTerminationReason.SUCCEEDED
assert call_kwargs["custom_metadata"] == "$.batch_id=='x'"


@pytest.mark.unit
def test_results_omits_none_filters(app_run, mock_api) -> None:
"""When state/termination_reason/custom_metadata are not provided they must not appear in API call."""
mock_api.list_run_items_v1_runs_run_id_items_get.return_value = []

list(app_run.results())

call_kwargs = mock_api.list_run_items_v1_runs_run_id_items_get.call_args[1]
assert "state" not in call_kwargs
assert "termination_reason" not in call_kwargs
assert "custom_metadata" not in call_kwargs
Loading