Skip to content

Commit f137f6b

Browse files
committed
Added sorting functionality to low level API
1 parent 3370fed commit f137f6b

File tree

9 files changed

+199
-15
lines changed

9 files changed

+199
-15
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

@@ -119,6 +133,7 @@ def get(
119133
cls,
120134
count: int | None = None,
121135
offset: int | None = None,
136+
sorting: list[ArtifactSort] | None = None,
122137
**kwargs,
123138
) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]:
124139
"""Returns artifacts associated with the current user.
@@ -129,6 +144,8 @@ def get(
129144
limit the number of results, default of None returns all.
130145
offset : int, optional
131146
start index for returned results, default of None starts at 0.
147+
sorting : list[dict] | None, optional
148+
list of sorting definitions in the form {'column': str, 'descending': bool}
132149
133150
Yields
134151
------
@@ -139,10 +156,15 @@ def get(
139156

140157
_class_instance = ArtifactBase(_local=True, _read_only=True)
141158
_url = f"{_class_instance._base_url}"
159+
_params = {"start": offset, "count": count}
160+
161+
if sorting:
162+
_params["sorting"] = json.dumps([sort.to_params() for sort in sorting])
163+
142164
_response = sv_get(
143165
_url,
144166
headers=_class_instance._headers,
145-
params={"start": offset, "count": count} | kwargs,
167+
params=_params | kwargs,
146168
)
147169
_label: str = _class_instance.__class__.__name__.lower()
148170
_label = _label.replace("base", "")

simvue/api/objects/base.py

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

130130

131+
class Sort(pydantic.BaseModel):
132+
column: str
133+
descending: bool = True
134+
135+
def to_params(self) -> dict[str, str]:
136+
return {"id": self.column, "desc": self.descending}
137+
138+
131139
class SimvueObject(abc.ABC):
132140
def __init__(
133141
self,
@@ -323,7 +331,13 @@ def get(
323331
**kwargs,
324332
) -> typing.Generator[tuple[str, T | None], None, None]:
325333
_class_instance = cls(_read_only=True, _local=True)
326-
if (_data := cls._get_all_objects(count, offset, **kwargs).get("data")) is None:
334+
if (
335+
_data := cls._get_all_objects(
336+
count=count,
337+
offset=offset,
338+
**kwargs,
339+
).get("data")
340+
) is None:
327341
raise RuntimeError(
328342
f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s"
329343
)
@@ -350,14 +364,19 @@ def count(cls, **kwargs) -> int:
350364

351365
@classmethod
352366
def _get_all_objects(
353-
cls, count: int | None, offset: int | None, **kwargs
367+
cls,
368+
count: int | None,
369+
offset: int | None,
370+
**kwargs,
354371
) -> dict[str, typing.Any]:
355372
_class_instance = cls(_read_only=True)
356373
_url = f"{_class_instance._base_url}"
374+
_params: dict[str, int | str] = {"start": offset, "count": count}
375+
357376
_response = sv_get(
358377
_url,
359378
headers=_class_instance._headers,
360-
params={"start": offset, "count": count} | kwargs,
379+
params=_params | kwargs,
361380
)
362381

363382
_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
@@ -44,6 +44,7 @@ def get(
4444
**kwargs,
4545
) -> typing.Generator[EventSet, None, None]:
4646
_class_instance = cls(_read_only=True, _local=True)
47+
4748
if (
4849
_data := cls._get_all_objects(count, offset, run=run_id, **kwargs).get(
4950
"data"

simvue/api/objects/folder.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,25 @@
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
23+
T = typing.TypeVar("T", bound="Folder")
24+
25+
26+
class FolderSort(Sort):
27+
@pydantic.field_validator("column")
28+
@classmethod
29+
def check_column(cls, column: str) -> str:
30+
if (
31+
column
32+
and column not in ("created", "modified", "path")
33+
and not column.startswith("metadata.")
34+
):
35+
raise ValueError(f"Invalid sort column for folders '{column}")
36+
return column
37+
2238

2339
class Folder(SimvueObject):
2440
"""
@@ -60,6 +76,17 @@ def new(
6076
"""Create a new Folder on the Simvue server with the given path"""
6177
return Folder(path=path, _read_only=False, _offline=offline, **kwargs)
6278

79+
@classmethod
80+
@pydantic.validate_call
81+
def get(
82+
cls,
83+
count: pydantic.PositiveInt | None = None,
84+
offset: pydantic.NonNegativeInt | None = None,
85+
sorting: list[FolderSort] | None = None,
86+
**kwargs,
87+
) -> typing.Generator[tuple[str, T | None], None, None]:
88+
return super().get(count=count, offset=offset, sorting=sorting)
89+
6390
@property
6491
@staging_check
6592
def tags(self) -> list[str]:

simvue/api/objects/run.py

Lines changed: 53 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,27 @@
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+
):
51+
raise ValueError(f"Invalid sort column for runs '{column}")
52+
53+
return column
54+
55+
3756
class Run(SimvueObject):
3857
"""Class for interacting with/creating runs on the server."""
3958

@@ -243,6 +262,39 @@ def get_alert_details(self) -> typing.Generator[dict[str, typing.Any], None, Non
243262
for alert in self._get_attribute("alerts"):
244263
yield alert["alert"]
245264

265+
@classmethod
266+
@pydantic.validate_call
267+
def get(
268+
cls,
269+
count: pydantic.PositiveInt | None = None,
270+
offset: pydantic.NonNegativeInt | None = None,
271+
sorting: list[RunSort] | None = None,
272+
**kwargs,
273+
) -> typing.Generator[tuple[str, T | None], None, None]:
274+
"""Get runs from the server.
275+
276+
Parameters
277+
----------
278+
count : int, optional
279+
limit the number of objects returned, default no limit.
280+
offset : int, optional
281+
start index for results, default is 0.
282+
sorting : list[dict] | None, optional
283+
list of sorting definitions in the form {'column': str, 'descending': bool}
284+
285+
Yields
286+
------
287+
tuple[str, Run]
288+
id of run
289+
Run object representing object on server
290+
"""
291+
_params: dict[str, str] = {}
292+
293+
if sorting:
294+
_params["sorting"] = json.dumps([i.to_params() for i in sorting])
295+
296+
return super().get(count=count, offset=offset, **_params)
297+
246298
@alerts.setter
247299
@write_only
248300
@pydantic.validate_call

0 commit comments

Comments
 (0)