Skip to content
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
6 changes: 3 additions & 3 deletions simvue/api/objects/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from simvue.api.url import URL

from .base import SimvueObject
from simvue.models import DATETIME_FORMAT, EventSet
from simvue.models import EventSet, simvue_timestamp
from simvue.api.request import get as sv_get, get_json_from_response

try:
Expand Down Expand Up @@ -98,8 +98,8 @@ def histogram(
"value difference must be greater than window"
)
_url: URL = self._base_url / "histogram"
_time_begin: str = timestamp_begin.strftime(DATETIME_FORMAT)
_time_end: str = timestamp_end.strftime(DATETIME_FORMAT)
_time_begin: str = simvue_timestamp(timestamp_begin)
_time_end: str = simvue_timestamp(timestamp_end)
_response = sv_get(
url=_url,
headers=self._headers,
Expand Down
6 changes: 5 additions & 1 deletion simvue/api/objects/folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,11 @@ def created(self) -> datetime.datetime | None:
"""Retrieve created datetime for the run"""
_created: str | None = self._get_attribute("created")
return (
datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None
datetime.datetime.strptime(_created, DATETIME_FORMAT).replace(
tzinfo=datetime.timezone.utc
)
if _created
else None
)


Expand Down
18 changes: 13 additions & 5 deletions simvue/api/objects/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
get_json_from_response,
)
from simvue.api.url import URL
from simvue.models import FOLDER_REGEX, NAME_REGEX, DATETIME_FORMAT
from simvue.models import FOLDER_REGEX, NAME_REGEX, DATETIME_FORMAT, simvue_timestamp

Status = typing.Literal[
"lost", "failed", "completed", "terminated", "running", "created"
Expand Down Expand Up @@ -478,14 +478,18 @@ def started(self) -> datetime.datetime | None:
"""
_started: str | None = self._get_attribute("started")
return (
datetime.datetime.strptime(_started, DATETIME_FORMAT) if _started else None
datetime.datetime.strptime(_started, DATETIME_FORMAT).replace(
tzinfo=datetime.timezone.utc
)
if _started
else None
)

@started.setter
@write_only
@pydantic.validate_call
def started(self, started: datetime.datetime) -> None:
self._staging["started"] = started.strftime(DATETIME_FORMAT)
self._staging["started"] = simvue_timestamp(started)

@property
@staging_check
Expand All @@ -498,14 +502,18 @@ def endtime(self) -> datetime.datetime | None:
"""
_endtime: str | None = self._get_attribute("endtime")
return (
datetime.datetime.strptime(_endtime, DATETIME_FORMAT) if _endtime else None
datetime.datetime.strptime(_endtime, DATETIME_FORMAT).replace(
tzinfo=datetime.timezone.utc
)
if _endtime
else None
)

@endtime.setter
@write_only
@pydantic.validate_call
def endtime(self, endtime: datetime.datetime) -> None:
self._staging["endtime"] = endtime.strftime(DATETIME_FORMAT)
self._staging["endtime"] = simvue_timestamp(endtime)

@property
def metrics(
Expand Down
6 changes: 5 additions & 1 deletion simvue/api/objects/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ def created(self) -> datetime.datetime | None:
"""Retrieve created datetime for the run"""
_created: str | None = self._get_attribute("created")
return (
datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None
datetime.datetime.strptime(_created, DATETIME_FORMAT).replace(
tzinfo=datetime.timezone.utc
)
if _created
else None
)

@classmethod
Expand Down
2 changes: 1 addition & 1 deletion simvue/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import logging
import pathlib

from simvue.utilities import simvue_timestamp
from simvue.models import simvue_timestamp

logger = logging.getLogger(__file__)

Expand Down
98 changes: 59 additions & 39 deletions simvue/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import typing
import numpy
import warnings
import pydantic


Expand All @@ -18,6 +19,56 @@
]


def validate_timestamp(timestamp: str, raise_except: bool = True) -> bool:
"""
Validate a user-provided timestamp
"""
try:
_ = datetime.datetime.strptime(timestamp, DATETIME_FORMAT)
except ValueError as e:
if raise_except:
raise e
return False

return True


@pydantic.validate_call(config={"validate_default": True})
def simvue_timestamp(
date_time: datetime.datetime
| typing.Annotated[str | None, pydantic.BeforeValidator(validate_timestamp)]
| None = None,
) -> str:
"""Return the Simvue valid timestamp

Parameters
----------
date_time: datetime.datetime | str, optional
if provided, the datetime object to convert,
else use current date and time
if a string assume to be local time.

Returns
-------
str
Datetime string valid for the Simvue server
"""
if isinstance(date_time, str):
warnings.warn(
"Timestamps as strings for object recording will be deprecated in Python API >= 2.3"
)
if not date_time:
date_time = datetime.datetime.now(datetime.timezone.utc)
elif isinstance(date_time, str):
_local_time = datetime.datetime.now().tzinfo
date_time = (
datetime.datetime.strptime(date_time, DATETIME_FORMAT)
.replace(tzinfo=_local_time)
.astimezone(datetime.timezone.utc)
)
return date_time.strftime(DATETIME_FORMAT)


# Pydantic class to validate run.init()
class RunInput(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="forbid")
Expand All @@ -33,44 +84,24 @@ class RunInput(pydantic.BaseModel):
class MetricSet(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="forbid")
time: pydantic.NonNegativeFloat | pydantic.NonNegativeInt
timestamp: str
timestamp: typing.Annotated[str | None, pydantic.BeforeValidator(simvue_timestamp)]
step: pydantic.NonNegativeInt
values: dict[str, int | float | bool]

@pydantic.field_validator("timestamp", mode="after")
@classmethod
def timestamp_str(cls, value: str) -> str:
try:
_ = datetime.datetime.strptime(value, DATETIME_FORMAT)
except ValueError as e:
raise AssertionError(
f"Invalid timestamp, expected form '{DATETIME_FORMAT}'"
) from e
return value


class GridMetricSet(pydantic.BaseModel):
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True, extra="forbid")
model_config = pydantic.ConfigDict(
arbitrary_types_allowed=True, extra="forbid", validate_default=True
)
time: pydantic.NonNegativeFloat | pydantic.NonNegativeInt
timestamp: str
timestamp: typing.Annotated[str | None, pydantic.BeforeValidator(simvue_timestamp)]
step: pydantic.NonNegativeInt
array: list | numpy.ndarray
array: list[float] | list[list[float]] | numpy.ndarray
grid: str
metric: str

@pydantic.field_validator("timestamp", mode="after")
@classmethod
def timestamp_str(cls, value: str) -> str:
try:
datetime.datetime.strptime(value, DATETIME_FORMAT)
except ValueError as e:
raise AssertionError(
f"Invalid timestamp, expected form '{DATETIME_FORMAT}'"
) from e
return value

@pydantic.field_serializer("array", when_used="always")
def serialize_array(self, value: numpy.ndarray | list, *_) -> list:
def serialize_array(self, value: numpy.ndarray | list[float], *_) -> list[float]:
if isinstance(value, list):
return value
return value.tolist()
Expand All @@ -79,15 +110,4 @@ def serialize_array(self, value: numpy.ndarray | list, *_) -> list:
class EventSet(pydantic.BaseModel):
model_config = pydantic.ConfigDict(extra="forbid")
message: str
timestamp: str

@pydantic.field_validator("timestamp", mode="after")
@classmethod
def timestamp_str(cls, value: str) -> str:
try:
datetime.datetime.strptime(value, DATETIME_FORMAT)
except ValueError as e:
raise AssertionError(
f"Invalid timestamp, expected form '{DATETIME_FORMAT}'"
) from e
return value
timestamp: typing.Annotated[str | None, pydantic.BeforeValidator(simvue_timestamp)]
Loading
Loading