Skip to content

Added sorting functionality #767

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 25, 2025
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
17 changes: 16 additions & 1 deletion simvue/api/objects/alert/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

import http
import pydantic
import datetime
import typing
from simvue.api.objects.base import SimvueObject, staging_check, write_only
from simvue.api.request import get as sv_get, get_json_from_response
from simvue.api.url import URL
from simvue.models import NAME_REGEX
from simvue.models import NAME_REGEX, DATETIME_FORMAT


class AlertBase(SimvueObject):
Expand Down Expand Up @@ -125,6 +126,20 @@ def abort(self) -> bool:
"""Retrieve if alert can abort simulations"""
return self._get_attribute("abort")

@property
@staging_check
def delay(self) -> int:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait what is this? You can delay the start of an alert?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the case then we should also add this functionality into the Run class in another MR :)

Copy link
Collaborator Author

@kzscisoft kzscisoft Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, this is just a value available in the RestAPI response, @alahiff ?

"""Retrieve delay value for this alert"""
return self._get_attribute("delay")

@property
def created(self) -> datetime.datetime | None:
"""Retrieve created datetime for the alert"""
_created: str | None = self._get_attribute("created")
return (
datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None
)

@abort.setter
@write_only
@pydantic.validate_call
Expand Down
21 changes: 20 additions & 1 deletion simvue/api/objects/alert/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

import typing
import http
import json

import pydantic

from simvue.api.objects.alert.user import UserAlert
from simvue.api.objects.base import Sort
from simvue.api.request import get_json_from_response
from simvue.api.request import get as sv_get
from .events import EventsAlert
Expand All @@ -21,6 +23,15 @@
AlertType = EventsAlert | UserAlert | MetricsThresholdAlert | MetricsRangeAlert


class AlertSort(Sort):
@pydantic.field_validator("column")
@classmethod
def check_column(cls, column: str) -> str:
if column and column not in ("name", "created"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be really useful to have a column which sorts by time set to critical, no idea if thats possible

raise ValueError(f"Invalid sort column for alerts '{column}'")
return column


class Alert:
"""Generic Simvue alert retrieval class"""

Expand Down Expand Up @@ -50,11 +61,13 @@ def __new__(cls, identifier: str, **kwargs) -> AlertType:
raise RuntimeError(f"Unknown source type '{_alert_pre.source}'")

@classmethod
@pydantic.validate_call
def get(
cls,
offline: bool = False,
count: int | None = None,
offset: int | None = None,
sorting: list[AlertSort] | None = None,
**kwargs,
) -> typing.Generator[tuple[str, AlertType], None, None]:
"""Fetch all alerts from the server for the current user.
Expand All @@ -65,6 +78,8 @@ def get(
limit the number of results, default of None returns all.
offset : int, optional
start index for returned results, default of None starts at 0.
sorting : list[dict] | None, optional
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really clear what this would be, what does each item in the list correspond to? Wouldnt you just want one sorting parameter for all the things youre getting?

Copy link
Collaborator Author

@kzscisoft kzscisoft Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the low level API so just mirrors the RestAPI which expects a list in the form [{"id": <column>, "desc": <if-descending-else-ascending>}]

list of sorting definitions in the form {'column': str, 'descending': bool}

Yields
------
Expand All @@ -80,11 +95,15 @@ def get(

_class_instance = AlertBase(_local=True, _read_only=True)
_url = f"{_class_instance._base_url}"
_params: dict[str, int | str] = {"start": offset, "count": count}

if sorting:
_params["sorting"] = json.dumps([sort.to_params() for sort in sorting])

_response = sv_get(
_url,
headers=_class_instance._headers,
params={"start": offset, "count": count} | kwargs,
params=_params | kwargs,
)

_label: str = _class_instance.__class__.__name__.lower()
Expand Down
30 changes: 26 additions & 4 deletions simvue/api/objects/artifact/fetch.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import http
import typing
import pydantic
import json

from simvue.api.objects.artifact.base import ArtifactBase
from simvue.api.objects.base import Sort
from .file import FileArtifact
from simvue.api.objects.artifact.object import ObjectArtifact
from simvue.api.request import get_json_from_response, get as sv_get
from simvue.api.url import URL
from simvue.exception import ObjectNotFoundError

import http
import typing
import pydantic

__all__ = ["Artifact"]


class ArtifactSort(Sort):
@pydantic.field_validator("column")
@classmethod
def check_column(cls, column: str) -> str:
if column and (
column not in ("name", "created") and not column.startswith("metadata.")
):
raise ValueError(f"Invalid sort column for artifacts '{column}'")
return column


class Artifact:
"""Generic Simvue artifact retrieval class"""

Expand Down Expand Up @@ -146,6 +160,7 @@ def get(
cls,
count: int | None = None,
offset: int | None = None,
sorting: list[ArtifactSort] | None = None,
**kwargs,
) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]:
"""Returns artifacts associated with the current user.
Expand All @@ -156,6 +171,8 @@ def get(
limit the number of results, default of None returns all.
offset : int, optional
start index for returned results, default of None starts at 0.
sorting : list[dict] | None, optional
list of sorting definitions in the form {'column': str, 'descending': bool}

Yields
------
Expand All @@ -166,10 +183,15 @@ def get(

_class_instance = ArtifactBase(_local=True, _read_only=True)
_url = f"{_class_instance._base_url}"
_params = {"start": offset, "count": count}

if sorting:
_params["sorting"] = json.dumps([sort.to_params() for sort in sorting])

_response = sv_get(
_url,
headers=_class_instance._headers,
params={"start": offset, "count": count} | kwargs,
params=_params | kwargs,
)
_label: str = _class_instance.__class__.__name__.lower()
_label = _label.replace("base", "")
Expand Down
25 changes: 22 additions & 3 deletions simvue/api/objects/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ def tenant(self, tenant: bool) -> None:
self._update_visibility("tenant", tenant)


class Sort(pydantic.BaseModel):
column: str
descending: bool = True

def to_params(self) -> dict[str, str]:
return {"id": self.column, "desc": self.descending}


class SimvueObject(abc.ABC):
def __init__(
self,
Expand Down Expand Up @@ -388,7 +396,13 @@ def get(
Generator[tuple[str, SimvueObject | None], None, None]
"""
_class_instance = cls(_read_only=True, _local=True)
if (_data := cls._get_all_objects(count, offset, **kwargs).get("data")) is None:
if (
_data := cls._get_all_objects(
count=count,
offset=offset,
**kwargs,
).get("data")
) is None:
raise RuntimeError(
f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s"
)
Expand Down Expand Up @@ -422,14 +436,19 @@ def count(cls, **kwargs) -> int:

@classmethod
def _get_all_objects(
cls, count: int | None, offset: int | None, **kwargs
cls,
count: int | None,
offset: int | None,
**kwargs,
) -> dict[str, typing.Any]:
_class_instance = cls(_read_only=True)
_url = f"{_class_instance._base_url}"
_params: dict[str, int | str] = {"start": offset, "count": count}

_response = sv_get(
_url,
headers=_class_instance._headers,
params={"start": offset, "count": count} | kwargs,
params=_params | kwargs,
)

_label = _class_instance.__class__.__name__.lower()
Expand Down
1 change: 1 addition & 0 deletions simvue/api/objects/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def get(
**kwargs,
) -> typing.Generator[EventSet, None, None]:
_class_instance = cls(_read_only=True, _local=True)

if (
_data := cls._get_all_objects(count, offset, run=run_id, **kwargs).get(
"data"
Expand Down
35 changes: 34 additions & 1 deletion simvue/api/objects/folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@

from simvue.exception import ObjectNotFoundError

from .base import SimvueObject, staging_check, write_only
from .base import SimvueObject, staging_check, write_only, Sort
from simvue.models import FOLDER_REGEX, DATETIME_FORMAT

# Need to use this inside of Generator typing to fix bug present in Python 3.10 - see issue #745
try:
from typing import Self
except ImportError:
Expand All @@ -28,6 +29,22 @@
__all__ = ["Folder"]


T = typing.TypeVar("T", bound="Folder")


class FolderSort(Sort):
@pydantic.field_validator("column")
@classmethod
def check_column(cls, column: str) -> str:
if (
column
and column not in ("created", "modified", "path")
and not column.startswith("metadata.")
):
raise ValueError(f"Invalid sort column for folders '{column}")
return column


class Folder(SimvueObject):
"""
Simvue Folder
Expand Down Expand Up @@ -68,6 +85,22 @@ def new(
"""Create a new Folder on the Simvue server with the given path"""
return Folder(path=path, _read_only=False, _offline=offline, **kwargs)

@classmethod
@pydantic.validate_call
def get(
cls,
count: pydantic.PositiveInt | None = None,
offset: pydantic.NonNegativeInt | None = None,
sorting: list[FolderSort] | None = None,
**kwargs,
) -> typing.Generator[tuple[str, T | None], None, None]:
_params: dict[str, str] = kwargs

if sorting:
_params["sorting"] = json.dumps([i.to_params() for i in sorting])

return super().get(count=count, offset=offset, **_params)

@property
@staging_check
def tags(self) -> list[str]:
Expand Down
55 changes: 54 additions & 1 deletion simvue/api/objects/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
import pydantic
import datetime
import time
import json

try:
from typing import Self
except ImportError:
from typing_extensions import Self

from .base import SimvueObject, staging_check, Visibility, write_only
from .base import SimvueObject, Sort, staging_check, Visibility, write_only
from simvue.api.request import (
get as sv_get,
put as sv_put,
Expand All @@ -31,9 +32,28 @@
"lost", "failed", "completed", "terminated", "running", "created"
]

# Need to use this inside of Generator typing to fix bug present in Python 3.10 - see issue #745
T = typing.TypeVar("T", bound="Run")

__all__ = ["Run"]


class RunSort(Sort):
@pydantic.field_validator("column")
@classmethod
def check_column(cls, column: str) -> str:
if (
column
and column != "name"
and not column.startswith("metrics")
and not column.startswith("metadata.")
and column not in ("created", "started", "endtime", "modified")
):
raise ValueError(f"Invalid sort column for runs '{column}'")

return column


class Run(SimvueObject):
"""Class for directly interacting with/creating runs on the server."""

Expand Down Expand Up @@ -292,6 +312,39 @@ def alerts(self) -> list[str]:

return [alert["id"] for alert in self.get_alert_details()]

@classmethod
@pydantic.validate_call
def get(
cls,
count: pydantic.PositiveInt | None = None,
offset: pydantic.NonNegativeInt | None = None,
sorting: list[RunSort] | None = None,
**kwargs,
) -> typing.Generator[tuple[str, T | None], None, None]:
"""Get runs from the server.

Parameters
----------
count : int, optional
limit the number of objects returned, default no limit.
offset : int, optional
start index for results, default is 0.
sorting : list[dict] | None, optional
list of sorting definitions in the form {'column': str, 'descending': bool}

Yields
------
tuple[str, Run]
id of run
Run object representing object on server
"""
_params: dict[str, str] = kwargs

if sorting:
_params["sorting"] = json.dumps([i.to_params() for i in sorting])

return super().get(count=count, offset=offset, **_params)

@alerts.setter
@write_only
@pydantic.validate_call
Expand Down
Loading