Skip to content

Commit 8a2c080

Browse files
committed
πŸ› Ensure validator runs on defaults for timestamp
1 parent a423428 commit 8a2c080

File tree

10 files changed

+88
-103
lines changed

10 files changed

+88
-103
lines changed

β€Žsimvue/api/objects/events.pyβ€Ž

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@
1414
import pydantic
1515

1616
from simvue.api.url import URL
17-
from simvue.utilities import simvue_timestamp
1817

1918
from .base import SimvueObject
20-
from simvue.models import EventSet
19+
from simvue.models import EventSet, simvue_timestamp
2120
from simvue.api.request import get as sv_get, get_json_from_response
2221

2322
try:

β€Žsimvue/api/objects/run.pyβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import datetime
1515
import json
1616

17-
from simvue.utilities import simvue_timestamp
17+
from simvue.models import simvue_timestamp
1818

1919
try:
2020
from typing import Self
@@ -471,7 +471,9 @@ def runtime(self) -> datetime.datetime | None:
471471
"""Retrieve execution time for the run"""
472472
_runtime: str | None = self._get_attribute("runtime")
473473
return (
474-
_runtime.strptime(_runtime, "%H:%M:%S.%f").astimezone(datetime.timezone.utc)
474+
datetime.datetime.strptime(_runtime, "%H:%M:%S.%f").astimezone(
475+
datetime.timezone.utc
476+
)
475477
if _runtime
476478
else None
477479
)

β€Žsimvue/metadata.pyβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717
import pathlib
1818

19-
from simvue.utilities import simvue_timestamp
19+
from simvue.models import simvue_timestamp
2020

2121
logger = logging.getLogger(__file__)
2222

β€Žsimvue/models.pyβ€Ž

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,51 @@
1818
]
1919

2020

21+
def validate_timestamp(timestamp: str, raise_except: bool = True) -> bool:
22+
"""
23+
Validate a user-provided timestamp
24+
"""
25+
try:
26+
_ = datetime.datetime.strptime(timestamp, DATETIME_FORMAT)
27+
except ValueError as e:
28+
if raise_except:
29+
raise e
30+
return False
31+
32+
return True
33+
34+
35+
@pydantic.validate_call(config=pydantic.ConfigDict(validate_default=True))
36+
def simvue_timestamp(
37+
date_time: datetime.datetime
38+
| typing.Annotated[str, validate_timestamp]
39+
| None = None,
40+
) -> str:
41+
"""Return the Simvue valid timestamp
42+
43+
Parameters
44+
----------
45+
date_time: datetime.datetime | str, optional
46+
if provided, the datetime object to convert, else use current date and time
47+
48+
Returns
49+
-------
50+
str
51+
Datetime string valid for the Simvue server
52+
"""
53+
if not date_time:
54+
date_time = datetime.datetime.now(datetime.timezone.utc)
55+
elif isinstance(date_time, str):
56+
# If the user specifies the 'naive' datetime string expected by the server
57+
# it is assumed that this is in their local timezone
58+
_local_timezone = datetime.datetime.now().astimezone().tzinfo
59+
date_time = datetime.datetime.strptime(date_time, DATETIME_FORMAT).replace(
60+
tzinfo=_local_timezone
61+
)
62+
_utc_datetime = date_time.astimezone(datetime.timezone.utc)
63+
return _utc_datetime.strftime(DATETIME_FORMAT)
64+
65+
2166
# Pydantic class to validate run.init()
2267
class RunInput(pydantic.BaseModel):
2368
model_config = pydantic.ConfigDict(extra="forbid")
@@ -33,42 +78,20 @@ class RunInput(pydantic.BaseModel):
3378
class MetricSet(pydantic.BaseModel):
3479
model_config = pydantic.ConfigDict(extra="forbid")
3580
time: pydantic.NonNegativeFloat | pydantic.NonNegativeInt
36-
timestamp: str
81+
timestamp: typing.Annotated[str, validate_timestamp]
3782
step: pydantic.NonNegativeInt
3883
values: dict[str, int | float | bool]
3984

40-
@pydantic.field_validator("timestamp", mode="after")
41-
@classmethod
42-
def timestamp_str(cls, value: str) -> str:
43-
try:
44-
_ = datetime.datetime.strptime(value, DATETIME_FORMAT)
45-
except ValueError as e:
46-
raise AssertionError(
47-
f"Invalid timestamp, expected form '{DATETIME_FORMAT}'"
48-
) from e
49-
return value
50-
5185

5286
class GridMetricSet(pydantic.BaseModel):
5387
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True, extra="forbid")
5488
time: pydantic.NonNegativeFloat | pydantic.NonNegativeInt
55-
timestamp: str
89+
timestamp: typing.Annotated[str, validate_timestamp]
5690
step: pydantic.NonNegativeInt
5791
array: list | numpy.ndarray
5892
grid: str
5993
metric: str
6094

61-
@pydantic.field_validator("timestamp", mode="after")
62-
@classmethod
63-
def timestamp_str(cls, value: str) -> str:
64-
try:
65-
_ = datetime.datetime.strptime(value, DATETIME_FORMAT)
66-
except ValueError as e:
67-
raise AssertionError(
68-
f"Invalid timestamp, expected form '{DATETIME_FORMAT}'"
69-
) from e
70-
return value
71-
7295
@pydantic.field_serializer("array", when_used="always")
7396
def serialize_array(self, value: numpy.ndarray | list, *_) -> list:
7497
if isinstance(value, list):
@@ -79,15 +102,4 @@ def serialize_array(self, value: numpy.ndarray | list, *_) -> list:
79102
class EventSet(pydantic.BaseModel):
80103
model_config = pydantic.ConfigDict(extra="forbid")
81104
message: str
82-
timestamp: str
83-
84-
@pydantic.field_validator("timestamp", mode="after")
85-
@classmethod
86-
def timestamp_str(cls, value: str) -> str:
87-
try:
88-
_ = datetime.datetime.strptime(value, DATETIME_FORMAT)
89-
except ValueError as e:
90-
raise AssertionError(
91-
f"Invalid timestamp, expected form '{DATETIME_FORMAT}'"
92-
) from e
93-
return value
105+
timestamp: typing.Annotated[str | None, simvue_timestamp]

β€Žsimvue/run.pyβ€Ž

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,13 @@
4141
from .factory.dispatch import Dispatcher
4242
from .executor import Executor, get_current_shell
4343
from .metrics import SystemResourceMeasurement
44-
from .models import FOLDER_REGEX, NAME_REGEX, MetricKeyString
44+
from .models import simvue_timestamp, FOLDER_REGEX, NAME_REGEX, MetricKeyString
4545
from .system import get_system
4646
from .metadata import git_info, environment
4747
from .eco import CO2Monitor
4848
from .utilities import (
4949
skip_if_failed,
5050
validate_timestamp,
51-
simvue_timestamp,
5251
)
5352
from .api.objects import (
5453
Run as RunObject,
@@ -281,11 +280,6 @@ def duration(self) -> float:
281280
"""Return current run duration"""
282281
return time.time() - self._start_time
283282

284-
@property
285-
def time_stamp(self) -> str:
286-
"""Return current timestamp"""
287-
return simvue_timestamp()
288-
289283
@property
290284
def processes(self) -> list[psutil.Process]:
291285
"""Create an array containing a list of processes"""
@@ -1256,22 +1250,30 @@ def update_tags(self, tags: list[str]) -> bool:
12561250

12571251
@skip_if_failed("_aborted", "_suppress_errors", False)
12581252
@check_run_initialised
1259-
@pydantic.validate_call
1260-
def log_event(self, message: str, timestamp: str | None = None) -> bool:
1253+
@pydantic.validate_call(config=pydantic.ConfigDict(validate_default=True))
1254+
def log_event(
1255+
self,
1256+
message: str,
1257+
timestamp: typing.Annotated[
1258+
str | None, pydantic.BeforeValidator(simvue_timestamp)
1259+
] = None,
1260+
) -> bool:
12611261
"""Log event to the server
12621262
12631263
Parameters
12641264
----------
12651265
message : str
12661266
event message to log
12671267
timestamp : str, optional
1268-
manually specify the time stamp for this log, by default None
1268+
manually specify the Simvue time stamp for this log, by default None
12691269
12701270
Returns
12711271
-------
12721272
bool
12731273
whether event log was successful
12741274
"""
1275+
if not timestamp:
1276+
raise Exception
12751277
if self._aborted:
12761278
return False
12771279

@@ -1287,22 +1289,24 @@ def log_event(self, message: str, timestamp: str | None = None) -> bool:
12871289
self._error("Cannot log events when not in the running state")
12881290
return False
12891291

1290-
if timestamp and not validate_timestamp(timestamp):
1291-
self._error("Invalid timestamp format")
1292-
return False
1293-
1294-
_data = {"message": message, "timestamp": timestamp or self.time_stamp}
1292+
_data = {
1293+
"message": message,
1294+
"timestamp": timestamp,
1295+
}
12951296
self._dispatcher.add_item(_data, "events", self._queue_blocking)
12961297

12971298
return True
12981299

1300+
@pydantic.validate_call(config=pydantic.ConfigDict(validate_default=True))
12991301
def _add_metrics_to_dispatch(
13001302
self,
13011303
metrics: dict[str, int | float],
13021304
*,
13031305
step: int | None = None,
13041306
time: float | None = None,
1305-
timestamp: str | None = None,
1307+
timestamp: typing.Annotated[
1308+
str | None, pydantic.BeforeValidator(simvue_timestamp)
1309+
] = None,
13061310
join_on_fail: bool = True,
13071311
) -> bool:
13081312
if self._user_config.run.mode == "disabled":
@@ -1326,34 +1330,37 @@ def _add_metrics_to_dispatch(
13261330
)
13271331
return False
13281332

1329-
if timestamp and not validate_timestamp(timestamp):
1333+
if timestamp and not validate_timestamp(timestamp, raise_except=False):
13301334
self._error("Invalid timestamp format", join_on_fail)
13311335
return False
13321336

13331337
_data: dict[str, typing.Any] = {
13341338
"values": metrics,
13351339
"time": time if time is not None else self.duration,
1336-
"timestamp": timestamp if timestamp is not None else self.time_stamp,
1340+
"timestamp": timestamp,
13371341
"step": step if step is not None else self._step,
13381342
}
13391343
self._dispatcher.add_item(_data, "metrics_regular", self._queue_blocking)
13401344

13411345
return True
13421346

1347+
@pydantic.validate_call(config=pydantic.ConfigDict(validate_default=True))
13431348
def _add_tensors_to_dispatch(
13441349
self,
13451350
tensors: dict[str, int | float],
13461351
*,
13471352
step: int | None = None,
13481353
time: float | None = None,
1349-
timestamp: str | None = None,
1354+
timestamp: typing.Annotated[
1355+
str | None, pydantic.BeforeValidator(simvue_timestamp)
1356+
] = None,
13501357
join_on_fail: bool = True,
13511358
) -> bool:
13521359
for tensor, array in tensors.items():
13531360
_data: dict[str, typing.Any] = {
13541361
"array": array,
13551362
"time": time if time is not None else self.duration,
1356-
"timestamp": timestamp if timestamp is not None else self.time_stamp,
1363+
"timestamp": timestamp,
13571364
"step": step if step is not None else self._step,
13581365
"grid": self._grids[tensor]["id"],
13591366
"metric": tensor,
@@ -1389,7 +1396,7 @@ def _add_values_to_dispatch(
13891396
)
13901397
return False
13911398

1392-
if timestamp and not validate_timestamp(timestamp):
1399+
if timestamp and not validate_timestamp(timestamp, raise_except=False):
13931400
self._error("Invalid timestamp format", join_on_fail)
13941401
return False
13951402

@@ -1549,7 +1556,7 @@ def log_metrics(
15491556
)
15501557
return False
15511558

1552-
if timestamp and not validate_timestamp(timestamp):
1559+
if timestamp and not validate_timestamp(timestamp, raise_except=False):
15531560
self._error("Invalid timestamp format", timestamp)
15541561
return False
15551562

β€Žsimvue/utilities.pyβ€Ž

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import datetime
21
import hashlib
32
import logging
43
import json
@@ -14,9 +13,6 @@
1413
import jwt
1514
from deepmerge import Merger
1615

17-
from datetime import timezone
18-
from simvue.models import DATETIME_FORMAT
19-
2016

2117
CHECKSUM_BLOCK_SIZE = 4096
2218
EXTRAS: tuple[str, ...] = ("plot", "torch")
@@ -120,7 +116,7 @@ def parse_validation_response(
120116
input_arg = None if obj_type == "missing" else input_arg[loc]
121117
except TypeError:
122118
break
123-
information.append(input_arg)
119+
information.append(str(input_arg))
124120

125121
# Limit message to be 60 characters
126122
msg: str = issue["msg"][:60]
@@ -372,37 +368,6 @@ def calculate_sha256(filename: str | typing.Any, is_file: bool) -> str | None:
372368
return sha256_hash.hexdigest()
373369

374370

375-
def validate_timestamp(timestamp):
376-
"""
377-
Validate a user-provided timestamp
378-
"""
379-
try:
380-
_ = datetime.datetime.strptime(timestamp, DATETIME_FORMAT)
381-
except ValueError:
382-
return False
383-
384-
return True
385-
386-
387-
def simvue_timestamp(date_time: datetime.datetime | None = None) -> str:
388-
"""Return the Simvue valid timestamp
389-
390-
Parameters
391-
----------
392-
date_time: datetime.datetime, optional
393-
if provided, the datetime object to convert, else use current date and time
394-
395-
Returns
396-
-------
397-
str
398-
Datetime string valid for the Simvue server
399-
"""
400-
if not date_time:
401-
date_time = datetime.datetime.now(timezone.utc)
402-
_utc_datetime = date_time.astimezone(datetime.timezone.utc)
403-
return _utc_datetime.strftime(DATETIME_FORMAT)
404-
405-
406371
@functools.lru_cache
407372
def get_mimetypes() -> list[str]:
408373
"""Returns a list of allowed MIME types"""

β€Žtests/functional/test_run_class.pyβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import simvue.config.user as sv_cfg
3131

3232
from simvue.api.objects import Run as RunObject
33-
from simvue.utilities import simvue_timestamp
33+
from simvue.models import simvue_timestamp
3434

3535
if typing.TYPE_CHECKING:
3636
from .conftest import CountingLogHandler

β€Žtests/unit/test_events.pyβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from simvue.api.objects import Events, Folder, Run
99
from simvue.models import DATETIME_FORMAT
1010
from simvue.sender import sender
11-
from simvue.utilities import simvue_timestamp
11+
from simvue.models import simvue_timestamp
1212

1313
@pytest.mark.api
1414
@pytest.mark.online

β€Žtests/unit/test_grids.pyβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from simvue.run import Run as sv_Run
1515
from simvue.sender import sender
1616
from simvue.client import Client
17-
from simvue.utilities import simvue_timestamp
17+
from simvue.models import simvue_timestamp
1818

1919
@pytest.mark.api
2020
@pytest.mark.online

0 commit comments

Comments
Β (0)