Skip to content

Commit d50141b

Browse files
authored
Merge pull request #876 from simvue-io/kzscisoft/use-utc
⚡️ Use UTC for all timestamps
2 parents 47b529c + 806ba68 commit d50141b

28 files changed

+217
-189
lines changed

simvue/api/objects/events.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from simvue.api.url import URL
1717

1818
from .base import SimvueObject
19-
from simvue.models import DATETIME_FORMAT, EventSet
19+
from simvue.models import EventSet, simvue_timestamp
2020
from simvue.api.request import get as sv_get, get_json_from_response
2121

2222
try:
@@ -98,8 +98,8 @@ def histogram(
9898
"value difference must be greater than window"
9999
)
100100
_url: URL = self._base_url / "histogram"
101-
_time_begin: str = timestamp_begin.strftime(DATETIME_FORMAT)
102-
_time_end: str = timestamp_end.strftime(DATETIME_FORMAT)
101+
_time_begin: str = simvue_timestamp(timestamp_begin)
102+
_time_end: str = simvue_timestamp(timestamp_end)
103103
_response = sv_get(
104104
url=_url,
105105
headers=self._headers,

simvue/api/objects/folder.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,11 @@ def created(self) -> datetime.datetime | None:
226226
"""Retrieve created datetime for the run"""
227227
_created: str | None = self._get_attribute("created")
228228
return (
229-
datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None
229+
datetime.datetime.strptime(_created, DATETIME_FORMAT).replace(
230+
tzinfo=datetime.timezone.utc
231+
)
232+
if _created
233+
else None
230234
)
231235

232236

simvue/api/objects/run.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
get_json_from_response,
3636
)
3737
from simvue.api.url import URL
38-
from simvue.models import FOLDER_REGEX, NAME_REGEX, DATETIME_FORMAT
38+
from simvue.models import FOLDER_REGEX, NAME_REGEX, DATETIME_FORMAT, simvue_timestamp
3939

4040
Status = typing.Literal[
4141
"lost", "failed", "completed", "terminated", "running", "created"
@@ -478,14 +478,18 @@ def started(self) -> datetime.datetime | None:
478478
"""
479479
_started: str | None = self._get_attribute("started")
480480
return (
481-
datetime.datetime.strptime(_started, DATETIME_FORMAT) if _started else None
481+
datetime.datetime.strptime(_started, DATETIME_FORMAT).replace(
482+
tzinfo=datetime.timezone.utc
483+
)
484+
if _started
485+
else None
482486
)
483487

484488
@started.setter
485489
@write_only
486490
@pydantic.validate_call
487491
def started(self, started: datetime.datetime) -> None:
488-
self._staging["started"] = started.strftime(DATETIME_FORMAT)
492+
self._staging["started"] = simvue_timestamp(started)
489493

490494
@property
491495
@staging_check
@@ -498,14 +502,18 @@ def endtime(self) -> datetime.datetime | None:
498502
"""
499503
_endtime: str | None = self._get_attribute("endtime")
500504
return (
501-
datetime.datetime.strptime(_endtime, DATETIME_FORMAT) if _endtime else None
505+
datetime.datetime.strptime(_endtime, DATETIME_FORMAT).replace(
506+
tzinfo=datetime.timezone.utc
507+
)
508+
if _endtime
509+
else None
502510
)
503511

504512
@endtime.setter
505513
@write_only
506514
@pydantic.validate_call
507515
def endtime(self, endtime: datetime.datetime) -> None:
508-
self._staging["endtime"] = endtime.strftime(DATETIME_FORMAT)
516+
self._staging["endtime"] = simvue_timestamp(endtime)
509517

510518
@property
511519
def metrics(

simvue/api/objects/tag.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ def created(self) -> datetime.datetime | None:
100100
"""Retrieve created datetime for the run"""
101101
_created: str | None = self._get_attribute("created")
102102
return (
103-
datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None
103+
datetime.datetime.strptime(_created, DATETIME_FORMAT).replace(
104+
tzinfo=datetime.timezone.utc
105+
)
106+
if _created
107+
else None
104108
)
105109

106110
@classmethod

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: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import typing
33
import numpy
4+
import warnings
45
import pydantic
56

67

@@ -18,6 +19,56 @@
1819
]
1920

2021

22+
def validate_timestamp(timestamp: str, raise_except: bool = True) -> bool:
23+
"""
24+
Validate a user-provided timestamp
25+
"""
26+
try:
27+
_ = datetime.datetime.strptime(timestamp, DATETIME_FORMAT)
28+
except ValueError as e:
29+
if raise_except:
30+
raise e
31+
return False
32+
33+
return True
34+
35+
36+
@pydantic.validate_call(config={"validate_default": True})
37+
def simvue_timestamp(
38+
date_time: datetime.datetime
39+
| typing.Annotated[str | None, pydantic.BeforeValidator(validate_timestamp)]
40+
| None = None,
41+
) -> str:
42+
"""Return the Simvue valid timestamp
43+
44+
Parameters
45+
----------
46+
date_time: datetime.datetime | str, optional
47+
if provided, the datetime object to convert,
48+
else use current date and time
49+
if a string assume to be local time.
50+
51+
Returns
52+
-------
53+
str
54+
Datetime string valid for the Simvue server
55+
"""
56+
if isinstance(date_time, str):
57+
warnings.warn(
58+
"Timestamps as strings for object recording will be deprecated in Python API >= 2.3"
59+
)
60+
if not date_time:
61+
date_time = datetime.datetime.now(datetime.timezone.utc)
62+
elif isinstance(date_time, str):
63+
_local_time = datetime.datetime.now().tzinfo
64+
date_time = (
65+
datetime.datetime.strptime(date_time, DATETIME_FORMAT)
66+
.replace(tzinfo=_local_time)
67+
.astimezone(datetime.timezone.utc)
68+
)
69+
return date_time.strftime(DATETIME_FORMAT)
70+
71+
2172
# Pydantic class to validate run.init()
2273
class RunInput(pydantic.BaseModel):
2374
model_config = pydantic.ConfigDict(extra="forbid")
@@ -33,44 +84,24 @@ class RunInput(pydantic.BaseModel):
3384
class MetricSet(pydantic.BaseModel):
3485
model_config = pydantic.ConfigDict(extra="forbid")
3586
time: pydantic.NonNegativeFloat | pydantic.NonNegativeInt
36-
timestamp: str
87+
timestamp: typing.Annotated[str | None, pydantic.BeforeValidator(simvue_timestamp)]
3788
step: pydantic.NonNegativeInt
3889
values: dict[str, int | float | bool]
3990

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-
5191

5292
class GridMetricSet(pydantic.BaseModel):
53-
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True, extra="forbid")
93+
model_config = pydantic.ConfigDict(
94+
arbitrary_types_allowed=True, extra="forbid", validate_default=True
95+
)
5496
time: pydantic.NonNegativeFloat | pydantic.NonNegativeInt
55-
timestamp: str
97+
timestamp: typing.Annotated[str | None, pydantic.BeforeValidator(simvue_timestamp)]
5698
step: pydantic.NonNegativeInt
57-
array: list | numpy.ndarray
99+
array: list[float] | list[list[float]] | numpy.ndarray
58100
grid: str
59101
metric: str
60102

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-
72103
@pydantic.field_serializer("array", when_used="always")
73-
def serialize_array(self, value: numpy.ndarray | list, *_) -> list:
104+
def serialize_array(self, value: numpy.ndarray | list[float], *_) -> list[float]:
74105
if isinstance(value, list):
75106
return value
76107
return value.tolist()
@@ -79,15 +110,4 @@ def serialize_array(self, value: numpy.ndarray | list, *_) -> list:
79110
class EventSet(pydantic.BaseModel):
80111
model_config = pydantic.ConfigDict(extra="forbid")
81112
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
113+
timestamp: typing.Annotated[str | None, pydantic.BeforeValidator(simvue_timestamp)]

0 commit comments

Comments
 (0)