Skip to content

Commit 70cce60

Browse files
mjohanse-emrMichael Johansen
andauthored
Add a converter for datetime.datetime <--> Timestamp. (#123)
* Add a converter for datetime.datetime <--> Timestamp. Signed-off-by: Michael Johansen <[email protected]> * Update and run the all_types example. Signed-off-by: Michael Johansen <[email protected]> * Add time and date controls to the all_types example. Signed-off-by: Michael Johansen <[email protected]> * Rename converter to be specific to datetime.datetime. Signed-off-by: Michael Johansen <[email protected]> * Update all_types example so that datetime keeps data synchronization. Signed-off-by: Michael Johansen <[email protected]> * Remove unneeded lines from example. Add a unit test case. Signed-off-by: Michael Johansen <[email protected]> --------- Signed-off-by: Michael Johansen <[email protected]> Co-authored-by: Michael Johansen <[email protected]>
1 parent 67b57ce commit 70cce60

File tree

6 files changed

+63
-4
lines changed

6 files changed

+63
-4
lines changed

examples/all_types/all_types_panel.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""A Streamlit visualization panel for the all_types.py example script."""
22

3+
import datetime as dt
34
from enum import Enum, Flag
45

56
import streamlit as st
@@ -120,6 +121,11 @@
120121
st.number_input(label=name, value=default_value, key=name, format="%.2f")
121122
elif isinstance(default_value, str):
122123
st.text_input(label=name, value=default_value, key=name)
124+
elif isinstance(default_value, dt.datetime):
125+
date = st.date_input(label="date", value=default_value)
126+
time = st.time_input(label="time", value=default_value)
127+
datetime = dt.datetime.combine(date, time)
128+
panel.set_value(name, datetime)
123129

124130
with col3:
125131
st.write(panel.get_value(name, default_value=default_value))

examples/all_types/define_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Define types."""
22

3+
import datetime as dt
34
import enum
45

56
import numpy as np
@@ -70,6 +71,7 @@ class MyMixedEnum(enum.Enum):
7071
"float": 13.12,
7172
"int": 42,
7273
"str": "sample string",
74+
"dt_datetime": dt.datetime.now(),
7375
# supported enum and flag types
7476
"intflags": MyIntFlags.VALUE1 | MyIntFlags.VALUE4,
7577
"intenum": MyIntEnum.VALUE20,

src/nipanel/_convert.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from nipanel.converters.builtin import (
1313
BoolConverter,
1414
BytesConverter,
15+
DTDateTimeConverter,
1516
FloatConverter,
1617
IntConverter,
1718
StrConverter,
@@ -37,6 +38,7 @@
3738
FloatConverter(),
3839
IntConverter(),
3940
StrConverter(),
41+
DTDateTimeConverter(),
4042
# Containers next
4143
BoolCollectionConverter(),
4244
BytesCollectionConverter(),

src/nipanel/converters/builtin.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Classes to convert between builtin Python scalars and containers."""
22

3+
import datetime as dt
34
from collections.abc import Collection
45
from typing import Type
56

6-
from google.protobuf import wrappers_pb2
7+
from google.protobuf import timestamp_pb2, wrappers_pb2
78
from ni.panels.v1 import panel_types_pb2
89

910
from nipanel.converters import Converter
@@ -119,6 +120,30 @@ def to_python_value(self, protobuf_message: wrappers_pb2.StringValue) -> str:
119120
return protobuf_message.value
120121

121122

123+
class DTDateTimeConverter(Converter[dt.datetime, timestamp_pb2.Timestamp]):
124+
"""A converter for datetime.datetime types."""
125+
126+
@property
127+
def python_typename(self) -> str:
128+
"""The Python type that this converter handles."""
129+
return dt.datetime.__name__
130+
131+
@property
132+
def protobuf_message(self) -> Type[timestamp_pb2.Timestamp]:
133+
"""The type-specific protobuf message for the Python type."""
134+
return timestamp_pb2.Timestamp
135+
136+
def to_protobuf_message(self, python_value: dt.datetime) -> timestamp_pb2.Timestamp:
137+
"""Convert the Python dt.datetime to a protobuf timestamp_pb2.Timestamp."""
138+
ts = self.protobuf_message()
139+
ts.FromDatetime(python_value)
140+
return ts
141+
142+
def to_python_value(self, protobuf_message: timestamp_pb2.Timestamp) -> dt.datetime:
143+
"""Convert the protobuf timestamp_pb2.Timestamp to a Python dt.datetime."""
144+
return protobuf_message.ToDatetime()
145+
146+
122147
class BoolCollectionConverter(Converter[Collection[bool], panel_types_pb2.BoolCollection]):
123148
"""A converter for a Collection of bools."""
124149

tests/unit/test_convert.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import datetime as dt
12
from typing import Any, Collection, Union
23

34
import numpy as np
45
import pytest
5-
from google.protobuf import any_pb2, wrappers_pb2
6+
from google.protobuf import any_pb2, timestamp_pb2, wrappers_pb2
67
from google.protobuf.message import Message
78
from ni.panels.v1 import panel_types_pb2
89
from ni.protobuf.types.scalar_pb2 import ScalarData
@@ -53,6 +54,7 @@
5354
(tests.types.MixinIntEnum.VALUE11, "int"),
5455
(tests.types.MyStrEnum.VALUE1, "str"),
5556
(tests.types.MixinStrEnum.VALUE11, "str"),
57+
(dt.datetime.now(), "datetime"),
5658
([False, False], "Collection.bool"),
5759
([b"mystr", b"mystr"], "Collection.bytes"),
5860
([456.2, 1.0], "Collection.float"),
@@ -123,6 +125,16 @@ def test___python_builtin_scalar___to_any___valid_wrapperpb2_value(
123125
assert unpack_dest.value == expected_value
124126

125127

128+
def test___python_datetime_datetime___to_any___valid_timestamppb2_value() -> None:
129+
expected_value = dt.datetime.now()
130+
result = nipanel._convert.to_any(expected_value)
131+
unpack_dest = timestamp_pb2.Timestamp()
132+
_assert_any_and_unpack(result, unpack_dest)
133+
134+
assert isinstance(unpack_dest, timestamp_pb2.Timestamp)
135+
assert unpack_dest.ToDatetime() == expected_value
136+
137+
126138
@pytest.mark.parametrize(
127139
"proto_type, default_value, expected_value",
128140
[
@@ -175,6 +187,18 @@ def test___wrapperpb2_value___from_any___valid_python_value(
175187
assert result == expected_value
176188

177189

190+
def test___timestamppb2_timestamp___from_any___valid_python_value() -> None:
191+
expected_value = dt.datetime.now()
192+
pb_value = timestamp_pb2.Timestamp()
193+
pb_value.FromDatetime(expected_value)
194+
packed_any = _pack_into_any(pb_value)
195+
196+
result = nipanel._convert.from_any(packed_any)
197+
198+
assert isinstance(result, dt.datetime)
199+
assert result == expected_value
200+
201+
178202
@pytest.mark.parametrize(
179203
"proto_type, expected_value",
180204
[

tests/unit/test_streamlit_panel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import datetime as dt
12
import enum
2-
from datetime import datetime
33

44
import grpc
55
import pytest
@@ -266,6 +266,7 @@ def test___set_string_enum_type___get_value_with_int_enum_default___raises_excep
266266
3.14,
267267
True,
268268
b"robotext",
269+
dt.datetime.now(),
269270
],
270271
)
271272
def test___builtin_scalar_type___set_value___gets_same_value(
@@ -312,7 +313,6 @@ def test___enum_type___set_value___gets_same_value(
312313
@pytest.mark.parametrize(
313314
"value_payload",
314315
[
315-
datetime.now(),
316316
lambda x: x + 1,
317317
[1, "string"],
318318
["string", []],

0 commit comments

Comments
 (0)