Skip to content

Commit 258b91d

Browse files
committed
utils: date_value_to_int moved from _Entity #29
1 parent c48c828 commit 258b91d

File tree

3 files changed

+44
-46
lines changed

3 files changed

+44
-46
lines changed

objectbox/model/entity.py

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
import flatbuffers.flexbuffers
1818
from typing import Generic
1919
import numpy as np
20-
from math import floor
2120
from datetime import datetime, timezone
2221
from objectbox.c import *
2322
from objectbox.model.properties import Property
23+
from objectbox.utils import date_value_to_int
2424
import threading
2525

2626

@@ -137,34 +137,6 @@ def get_object_id(self, object) -> int:
137137
def set_object_id(self, object, id: int):
138138
setattr(object, self.id_property._name, id)
139139

140-
@staticmethod
141-
def date_value_to_int(value, multiplier: int) -> int:
142-
if isinstance(value, datetime):
143-
try:
144-
return round(value.timestamp() * multiplier) # timestamp returns seconds
145-
except OSError:
146-
# On Windows, timestamp() raises an OSError for naive datetime objects with dates is close to the epoch.
147-
# Thus, it is highly recommended to only use datetime *with* timezone information (no issue here).
148-
# See bug reports:
149-
# https://github.com/python/cpython/issues/81708 and https://github.com/python/cpython/issues/94414
150-
# The workaround is to go via timezone-aware datetime objects, which seem to work - with one caveat.
151-
local_tz = datetime.now().astimezone().tzinfo
152-
value = value.replace(tzinfo=local_tz)
153-
value = value.astimezone(timezone.utc)
154-
# Caveat: times may be off by; offset should be 0 but actually was seen at -3600 in CEST (Linux & Win).
155-
# See also https://stackoverflow.com/q/56931738/551269
156-
# So, let's check value 0 as a reference and use the resulting timestamp as an offset for correction.
157-
offset = datetime.fromtimestamp(0).replace(tzinfo=local_tz).astimezone(timezone.utc).timestamp()
158-
return round((value.timestamp() - offset) * multiplier) # timestamp returns seconds
159-
elif isinstance(value, float):
160-
return round(value * multiplier) # floats typically represent seconds
161-
elif isinstance(value, int): # Interpret ints as-is (without the multiplier); e.g. milliseconds or nanoseconds
162-
return value
163-
else:
164-
raise TypeError(
165-
f"Unsupported Python datetime type: {type(value)}. Please use datetime, float (seconds based) or "
166-
f"int (milliseconds for Date, nanoseconds for DateNano).")
167-
168140
def marshal(self, object, id: int) -> bytearray:
169141
if not hasattr(self._tl, "builder"):
170142
self._tl.builder = flatbuffers.Builder(256)
@@ -215,9 +187,9 @@ def marshal(self, object, id: int) -> bytearray:
215187
else:
216188
val = id if prop == self.id_property else self.get_value(object, prop)
217189
if prop._ob_type == OBXPropertyType_Date:
218-
val = self.date_value_to_int(val, 1000) # convert to milliseconds
190+
val = date_value_to_int(val, 1000) # convert to milliseconds
219191
elif prop._ob_type == OBXPropertyType_DateNano:
220-
val = self.date_value_to_int(val, 1000000000) # convert to nanoseconds
192+
val = date_value_to_int(val, 1000000000) # convert to nanoseconds
221193
builder.Prepend(prop._fb_type, val)
222194

223195
builder.Slot(prop._fb_slot)

objectbox/utils.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from objectbox.c import *
44
from objectbox.model.properties import VectorDistanceType
5-
5+
from datetime import datetime, timezone
66

77
def check_float_vector(vector: Union[np.ndarray, List[float]], vector_name: str):
88
""" Checks that the given vector is a float vector (either np.ndarray or Python's list). """
@@ -28,3 +28,30 @@ def vector_distance_f32(distance_type: VectorDistanceType,
2828
def vector_distance_to_relevance(distance_type: VectorDistanceType, distance: float) -> float:
2929
""" Converts the given distance to a relevance score in range [0.0, 1.0], according to its type. """
3030
return obx_vector_distance_to_relevance(distance_type, distance)
31+
32+
def date_value_to_int(value, multiplier: int) -> int:
33+
if isinstance(value, datetime):
34+
try:
35+
return round(value.timestamp() * multiplier) # timestamp returns seconds
36+
except OSError:
37+
# On Windows, timestamp() raises an OSError for naive datetime objects with dates is close to the epoch.
38+
# Thus, it is highly recommended to only use datetime *with* timezone information (no issue here).
39+
# See bug reports:
40+
# https://github.com/python/cpython/issues/81708 and https://github.com/python/cpython/issues/94414
41+
# The workaround is to go via timezone-aware datetime objects, which seem to work - with one caveat.
42+
local_tz = datetime.now().astimezone().tzinfo
43+
value = value.replace(tzinfo=local_tz)
44+
value = value.astimezone(timezone.utc)
45+
# Caveat: times may be off by; offset should be 0 but actually was seen at -3600 in CEST (Linux & Win).
46+
# See also https://stackoverflow.com/q/56931738/551269
47+
# So, let's check value 0 as a reference and use the resulting timestamp as an offset for correction.
48+
offset = datetime.fromtimestamp(0).replace(tzinfo=local_tz).astimezone(timezone.utc).timestamp()
49+
return round((value.timestamp() - offset) * multiplier) # timestamp returns seconds
50+
elif isinstance(value, float):
51+
return round(value * multiplier) # floats typically represent seconds
52+
elif isinstance(value, int): # Interpret ints as-is (without the multiplier); e.g. milliseconds or nanoseconds
53+
return value
54+
else:
55+
raise TypeError(
56+
f"Unsupported Python datetime type: {type(value)}. Please use datetime, float (seconds based) or "
57+
f"int (milliseconds for Date, nanoseconds for DateNano).")

tests/test_utils.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,23 @@
33

44
import pytest
55

6-
from objectbox.model.entity import _Entity
76
from objectbox.utils import *
87

98

109
def test_date_value_to_int__basics():
11-
assert _Entity.date_value_to_int(1234, 1000) == 1234
12-
assert _Entity.date_value_to_int(1234, 1000000000) == 1234
13-
assert _Entity.date_value_to_int(1234.0, 1000) == 1234000 # milliseconds
14-
assert _Entity.date_value_to_int(1234.0, 1000000000) == 1234000000000 # nanoseconds
10+
assert date_value_to_int(1234, 1000) == 1234
11+
assert date_value_to_int(1234, 1000000000) == 1234
12+
assert date_value_to_int(1234.0, 1000) == 1234000 # milliseconds
13+
assert date_value_to_int(1234.0, 1000000000) == 1234000000000 # nanoseconds
1514
dt = datetime.fromtimestamp(12345678) # May 1970; 1234 is too close to the epoch (special case for that below)
16-
assert _Entity.date_value_to_int(dt, 1000) == 12345678000 # milliseconds
15+
assert date_value_to_int(dt, 1000) == 12345678000 # milliseconds
1716

1817

1918
def test_date_value_to_int__close_to_epoch():
20-
assert _Entity.date_value_to_int(datetime.fromtimestamp(0, timezone.utc), 1000) == 0
21-
assert _Entity.date_value_to_int(datetime.fromtimestamp(1234, timezone.utc), 1000) == 1234000
22-
assert _Entity.date_value_to_int(datetime.fromtimestamp(0), 1000) == 0
23-
assert _Entity.date_value_to_int(datetime.fromtimestamp(1234), 1000) == 1234000
19+
assert date_value_to_int(datetime.fromtimestamp(0, timezone.utc), 1000) == 0
20+
assert date_value_to_int(datetime.fromtimestamp(1234, timezone.utc), 1000) == 1234000
21+
assert date_value_to_int(datetime.fromtimestamp(0), 1000) == 0
22+
assert date_value_to_int(datetime.fromtimestamp(1234), 1000) == 1234000
2423

2524
# "Return the local date corresponding to the POSIX timestamp"; but not always!? Was -1 hour off with CEST:
2625
dt0naive = datetime.fromtimestamp(0)
@@ -47,7 +46,7 @@ def test_date_value_to_int__close_to_epoch():
4746
# Non-Windows platforms should work fine
4847
assert dt.timestamp() == 1234
4948

50-
assert _Entity.date_value_to_int(dt, 1000) == 1234000 # milliseconds
49+
assert date_value_to_int(dt, 1000) == 1234000 # milliseconds
5150

5251

5352
def test_date_value_to_int__timezone():
@@ -62,8 +61,8 @@ def test_date_value_to_int__timezone():
6261

6362
# Actual test
6463
expected: int = 957184245123
65-
assert _Entity.date_value_to_int(dt_utc, 1000) == expected
66-
assert _Entity.date_value_to_int(dt_plus2, 1000) == expected
64+
assert date_value_to_int(dt_utc, 1000) == expected
65+
assert date_value_to_int(dt_plus2, 1000) == expected
6766

6867

6968
def test_date_value_to_int__naive():
@@ -76,7 +75,7 @@ def test_date_value_to_int__naive():
7675
assert dt_naive.timestamp() == dt_local.timestamp()
7776

7877
# Actual test
79-
assert _Entity.date_value_to_int(dt_naive, 1000) == _Entity.date_value_to_int(dt_local, 1000)
78+
assert date_value_to_int(dt_naive, 1000) == date_value_to_int(dt_local, 1000)
8079

8180

8281
def test_vector_distance_f32():

0 commit comments

Comments
 (0)