Skip to content

Commit 072eb35

Browse files
authored
Merge pull request #767 from simvue-io/feature/sorting
Added sorting functionality to low level API
2 parents d6c0fa1 + 33bf821 commit 072eb35

File tree

12 files changed

+399
-34
lines changed

12 files changed

+399
-34
lines changed

simvue/api/objects/alert/base.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
import http
1010
import pydantic
11+
import datetime
1112
import typing
1213
from simvue.api.objects.base import SimvueObject, staging_check, write_only
1314
from simvue.api.request import get as sv_get, get_json_from_response
1415
from simvue.api.url import URL
15-
from simvue.models import NAME_REGEX
16+
from simvue.models import NAME_REGEX, DATETIME_FORMAT
1617

1718

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

129+
@property
130+
@staging_check
131+
def delay(self) -> int:
132+
"""Retrieve delay value for this alert"""
133+
return self._get_attribute("delay")
134+
135+
@property
136+
def created(self) -> datetime.datetime | None:
137+
"""Retrieve created datetime for the alert"""
138+
_created: str | None = self._get_attribute("created")
139+
return (
140+
datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None
141+
)
142+
128143
@abort.setter
129144
@write_only
130145
@pydantic.validate_call

simvue/api/objects/alert/fetch.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88

99
import typing
1010
import http
11+
import json
1112

1213
import pydantic
1314

1415
from simvue.api.objects.alert.user import UserAlert
16+
from simvue.api.objects.base import Sort
1517
from simvue.api.request import get_json_from_response
1618
from simvue.api.request import get as sv_get
1719
from .events import EventsAlert
@@ -21,6 +23,15 @@
2123
AlertType = EventsAlert | UserAlert | MetricsThresholdAlert | MetricsRangeAlert
2224

2325

26+
class AlertSort(Sort):
27+
@pydantic.field_validator("column")
28+
@classmethod
29+
def check_column(cls, column: str) -> str:
30+
if column and column not in ("name", "created"):
31+
raise ValueError(f"Invalid sort column for alerts '{column}'")
32+
return column
33+
34+
2435
class Alert:
2536
"""Generic Simvue alert retrieval class"""
2637

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

5263
@classmethod
64+
@pydantic.validate_call
5365
def get(
5466
cls,
5567
offline: bool = False,
5668
count: int | None = None,
5769
offset: int | None = None,
70+
sorting: list[AlertSort] | None = None,
5871
**kwargs,
5972
) -> typing.Generator[tuple[str, AlertType], None, None]:
6073
"""Fetch all alerts from the server for the current user.
@@ -65,6 +78,8 @@ def get(
6578
limit the number of results, default of None returns all.
6679
offset : int, optional
6780
start index for returned results, default of None starts at 0.
81+
sorting : list[dict] | None, optional
82+
list of sorting definitions in the form {'column': str, 'descending': bool}
6883
6984
Yields
7085
------
@@ -80,11 +95,15 @@ def get(
8095

8196
_class_instance = AlertBase(_local=True, _read_only=True)
8297
_url = f"{_class_instance._base_url}"
98+
_params: dict[str, int | str] = {"start": offset, "count": count}
99+
100+
if sorting:
101+
_params["sorting"] = json.dumps([sort.to_params() for sort in sorting])
83102

84103
_response = sv_get(
85104
_url,
86105
headers=_class_instance._headers,
87-
params={"start": offset, "count": count} | kwargs,
106+
params=_params | kwargs,
88107
)
89108

90109
_label: str = _class_instance.__class__.__name__.lower()

simvue/api/objects/artifact/fetch.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
1+
import http
2+
import typing
3+
import pydantic
4+
import json
5+
16
from simvue.api.objects.artifact.base import ArtifactBase
7+
from simvue.api.objects.base import Sort
28
from .file import FileArtifact
39
from simvue.api.objects.artifact.object import ObjectArtifact
410
from simvue.api.request import get_json_from_response, get as sv_get
511
from simvue.api.url import URL
612
from simvue.exception import ObjectNotFoundError
713

8-
import http
9-
import typing
10-
import pydantic
1114

1215
__all__ = ["Artifact"]
1316

1417

18+
class ArtifactSort(Sort):
19+
@pydantic.field_validator("column")
20+
@classmethod
21+
def check_column(cls, column: str) -> str:
22+
if column and (
23+
column not in ("name", "created") and not column.startswith("metadata.")
24+
):
25+
raise ValueError(f"Invalid sort column for artifacts '{column}'")
26+
return column
27+
28+
1529
class Artifact:
1630
"""Generic Simvue artifact retrieval class"""
1731

@@ -146,6 +160,7 @@ def get(
146160
cls,
147161
count: int | None = None,
148162
offset: int | None = None,
163+
sorting: list[ArtifactSort] | None = None,
149164
**kwargs,
150165
) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]:
151166
"""Returns artifacts associated with the current user.
@@ -156,6 +171,8 @@ def get(
156171
limit the number of results, default of None returns all.
157172
offset : int, optional
158173
start index for returned results, default of None starts at 0.
174+
sorting : list[dict] | None, optional
175+
list of sorting definitions in the form {'column': str, 'descending': bool}
159176
160177
Yields
161178
------
@@ -166,10 +183,15 @@ def get(
166183

167184
_class_instance = ArtifactBase(_local=True, _read_only=True)
168185
_url = f"{_class_instance._base_url}"
186+
_params = {"start": offset, "count": count}
187+
188+
if sorting:
189+
_params["sorting"] = json.dumps([sort.to_params() for sort in sorting])
190+
169191
_response = sv_get(
170192
_url,
171193
headers=_class_instance._headers,
172-
params={"start": offset, "count": count} | kwargs,
194+
params=_params | kwargs,
173195
)
174196
_label: str = _class_instance.__class__.__name__.lower()
175197
_label = _label.replace("base", "")

simvue/api/objects/base.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ def tenant(self, tenant: bool) -> None:
162162
self._update_visibility("tenant", tenant)
163163

164164

165+
class Sort(pydantic.BaseModel):
166+
column: str
167+
descending: bool = True
168+
169+
def to_params(self) -> dict[str, str]:
170+
return {"id": self.column, "desc": self.descending}
171+
172+
165173
class SimvueObject(abc.ABC):
166174
def __init__(
167175
self,
@@ -388,7 +396,13 @@ def get(
388396
Generator[tuple[str, SimvueObject | None], None, None]
389397
"""
390398
_class_instance = cls(_read_only=True, _local=True)
391-
if (_data := cls._get_all_objects(count, offset, **kwargs).get("data")) is None:
399+
if (
400+
_data := cls._get_all_objects(
401+
count=count,
402+
offset=offset,
403+
**kwargs,
404+
).get("data")
405+
) is None:
392406
raise RuntimeError(
393407
f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s"
394408
)
@@ -422,14 +436,19 @@ def count(cls, **kwargs) -> int:
422436

423437
@classmethod
424438
def _get_all_objects(
425-
cls, count: int | None, offset: int | None, **kwargs
439+
cls,
440+
count: int | None,
441+
offset: int | None,
442+
**kwargs,
426443
) -> dict[str, typing.Any]:
427444
_class_instance = cls(_read_only=True)
428445
_url = f"{_class_instance._base_url}"
446+
_params: dict[str, int | str] = {"start": offset, "count": count}
447+
429448
_response = sv_get(
430449
_url,
431450
headers=_class_instance._headers,
432-
params={"start": offset, "count": count} | kwargs,
451+
params=_params | kwargs,
433452
)
434453

435454
_label = _class_instance.__class__.__name__.lower()

simvue/api/objects/events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def get(
4949
**kwargs,
5050
) -> typing.Generator[EventSet, None, None]:
5151
_class_instance = cls(_read_only=True, _local=True)
52+
5253
if (
5354
_data := cls._get_all_objects(count, offset, run=run_id, **kwargs).get(
5455
"data"

simvue/api/objects/folder.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616

1717
from simvue.exception import ObjectNotFoundError
1818

19-
from .base import SimvueObject, staging_check, write_only
19+
from .base import SimvueObject, staging_check, write_only, Sort
2020
from simvue.models import FOLDER_REGEX, DATETIME_FORMAT
2121

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

3031

32+
T = typing.TypeVar("T", bound="Folder")
33+
34+
35+
class FolderSort(Sort):
36+
@pydantic.field_validator("column")
37+
@classmethod
38+
def check_column(cls, column: str) -> str:
39+
if (
40+
column
41+
and column not in ("created", "modified", "path")
42+
and not column.startswith("metadata.")
43+
):
44+
raise ValueError(f"Invalid sort column for folders '{column}")
45+
return column
46+
47+
3148
class Folder(SimvueObject):
3249
"""
3350
Simvue Folder
@@ -68,6 +85,22 @@ def new(
6885
"""Create a new Folder on the Simvue server with the given path"""
6986
return Folder(path=path, _read_only=False, _offline=offline, **kwargs)
7087

88+
@classmethod
89+
@pydantic.validate_call
90+
def get(
91+
cls,
92+
count: pydantic.PositiveInt | None = None,
93+
offset: pydantic.NonNegativeInt | None = None,
94+
sorting: list[FolderSort] | None = None,
95+
**kwargs,
96+
) -> typing.Generator[tuple[str, T | None], None, None]:
97+
_params: dict[str, str] = kwargs
98+
99+
if sorting:
100+
_params["sorting"] = json.dumps([i.to_params() for i in sorting])
101+
102+
return super().get(count=count, offset=offset, **_params)
103+
71104
@property
72105
@staging_check
73106
def tags(self) -> list[str]:

simvue/api/objects/run.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@
1212
import pydantic
1313
import datetime
1414
import time
15+
import json
1516

1617
try:
1718
from typing import Self
1819
except ImportError:
1920
from typing_extensions import Self
2021

21-
from .base import SimvueObject, staging_check, Visibility, write_only
22+
from .base import SimvueObject, Sort, staging_check, Visibility, write_only
2223
from simvue.api.request import (
2324
get as sv_get,
2425
put as sv_put,
@@ -31,9 +32,28 @@
3132
"lost", "failed", "completed", "terminated", "running", "created"
3233
]
3334

35+
# Need to use this inside of Generator typing to fix bug present in Python 3.10 - see issue #745
36+
T = typing.TypeVar("T", bound="Run")
37+
3438
__all__ = ["Run"]
3539

3640

41+
class RunSort(Sort):
42+
@pydantic.field_validator("column")
43+
@classmethod
44+
def check_column(cls, column: str) -> str:
45+
if (
46+
column
47+
and column != "name"
48+
and not column.startswith("metrics")
49+
and not column.startswith("metadata.")
50+
and column not in ("created", "started", "endtime", "modified")
51+
):
52+
raise ValueError(f"Invalid sort column for runs '{column}'")
53+
54+
return column
55+
56+
3757
class Run(SimvueObject):
3858
"""Class for directly interacting with/creating runs on the server."""
3959

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

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

315+
@classmethod
316+
@pydantic.validate_call
317+
def get(
318+
cls,
319+
count: pydantic.PositiveInt | None = None,
320+
offset: pydantic.NonNegativeInt | None = None,
321+
sorting: list[RunSort] | None = None,
322+
**kwargs,
323+
) -> typing.Generator[tuple[str, T | None], None, None]:
324+
"""Get runs from the server.
325+
326+
Parameters
327+
----------
328+
count : int, optional
329+
limit the number of objects returned, default no limit.
330+
offset : int, optional
331+
start index for results, default is 0.
332+
sorting : list[dict] | None, optional
333+
list of sorting definitions in the form {'column': str, 'descending': bool}
334+
335+
Yields
336+
------
337+
tuple[str, Run]
338+
id of run
339+
Run object representing object on server
340+
"""
341+
_params: dict[str, str] = kwargs
342+
343+
if sorting:
344+
_params["sorting"] = json.dumps([i.to_params() for i in sorting])
345+
346+
return super().get(count=count, offset=offset, **_params)
347+
295348
@alerts.setter
296349
@write_only
297350
@pydantic.validate_call

0 commit comments

Comments
 (0)