Skip to content

Commit a2bd359

Browse files
committed
Merge branch '6-date-types' into 'main'
Add support for Date and DateNano types #6 Closes #6 See merge request objectbox/objectbox-python!6
2 parents e19c686 + 48b546e commit a2bd359

File tree

11 files changed

+150
-29
lines changed

11 files changed

+150
-29
lines changed

example/__main__.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
from cmd import Cmd
22
import objectbox
3-
import datetime
3+
import time
44
from example.model import *
55

66

77
# objectbox expects date timestamp in milliseconds since UNIX epoch
88
def now_ms() -> int:
9-
seconds: float = datetime.datetime.utcnow().timestamp()
10-
return round(seconds * 1000)
9+
return time.time_ns() / 1000000
1110

1211

1312
def format_date(timestamp_ms: int) -> str:
14-
return "" if timestamp_ms == 0 else str(datetime.datetime.fromtimestamp(timestamp_ms / 1000))
13+
return "" if timestamp_ms == 0 else time.ctime(timestamp_ms / 1000)
1514

1615

1716
class TasklistCmd(Cmd):

example/model.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ class Task:
66
id = Id(id=1, uid=1001)
77
text = Property(str, id=2, uid=1002)
88

9-
# TODO property type DATE
10-
date_created = Property(int, id=3, uid=1003)
11-
date_finished = Property(int, id=4, uid=1004)
9+
date_created = Property(int, type=PropertyType.date, id=3, uid=1003)
10+
date_finished = Property(int, type=PropertyType.date, id=4, uid=1004)
1211

1312

1413
def get_objectbox_model():
1514
m = Model()
1615
m.entity(Task, last_property_id=IdUid(4, 1004))
17-
m.last_entity_id = IdUid(1, 1)
16+
m.last_entity_id = IdUid(2, 2)
1817
return m

objectbox/c.py

+1
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ def c_voidp_as_bytes(voidp, size):
394394
OBXPropertyType_String = 9
395395
OBXPropertyType_Date = 10
396396
OBXPropertyType_Relation = 11
397+
OBXPropertyType_DateNano = 12
397398
OBXPropertyType_ByteVector = 23
398399
OBXPropertyType_IntVector = 26
399400
OBXPropertyType_LongVector = 27

objectbox/model/entity.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
import flatbuffers
1717
import numpy as np
18+
from math import floor
19+
from datetime import datetime
1820
from objectbox.c import *
1921
from objectbox.model.properties import Property
2022

@@ -88,8 +90,11 @@ def get_value(self, object, prop: Property):
8890
if prop._py_type == np.ndarray:
8991
if (val == np.array(prop)).all():
9092
return np.array([])
93+
elif prop._py_type == datetime:
94+
if val == prop:
95+
return datetime.fromtimestamp(0)
9196
elif val == prop:
92-
return prop._py_type() # default (empty) value for the given type
97+
return prop._py_type() # default (empty) value for the given type
9398
return val
9499

95100
def get_object_id(self, object) -> int:
@@ -130,8 +135,15 @@ def marshal(self, object, id: int) -> bytearray:
130135
if val:
131136
builder.PrependUOffsetTRelative(val)
132137
else:
133-
val = id if prop == self.id_property else self.get_value(
134-
object, prop)
138+
val = id if prop == self.id_property else self.get_value(object, prop)
139+
if prop._ob_type == OBXPropertyType_Date:
140+
if prop._py_type == datetime:
141+
val = val.timestamp() * 1000 # timestamp returns seconds, convert to milliseconds
142+
val = floor(val) # use floor to allow for float types
143+
elif prop._ob_type == OBXPropertyType_DateNano:
144+
if prop._py_type == datetime:
145+
val = val.timestamp() * 1000000000 # convert to nanoseconds
146+
val = floor(val) # use floor to allow for float types
135147
builder.Prepend(prop._fb_type, val)
136148

137149
builder.Slot(prop._fb_slot)
@@ -160,6 +172,12 @@ def unmarshal(self, data: bytes):
160172
size = table.VectorLen(o)
161173
# slice the vector as a requested type
162174
val = prop._py_type(table.Bytes[start:start+size])
175+
elif prop._ob_type == OBXPropertyType_Date and prop._py_type == datetime:
176+
table_val = table.Get(prop._fb_type, o + table.Pos)
177+
val = datetime.fromtimestamp(table_val/1000) if table_val != 0 else datetime.fromtimestamp(0) # default timestamp
178+
elif prop._ob_type == OBXPropertyType_DateNano and prop._py_type == datetime:
179+
table_val = table.Get(prop._fb_type, o + table.Pos)
180+
val = datetime.fromtimestamp(table_val/1000000000) if table_val != 0 else datetime.fromtimestamp(0) # default timestamp
163181
elif prop._ob_type == OBXPropertyType_IntVector:
164182
val = table.GetVectorAsNumpy(flatbuffers.number_types.Int32Flags, o)
165183
elif prop._ob_type == OBXPropertyType_LongVector:

objectbox/model/properties.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class PropertyType(IntEnum):
2929
float = OBXPropertyType_Float
3030
double = OBXPropertyType_Double
3131
string = OBXPropertyType_String
32-
# date = OBXPropertyType_Date
32+
date = OBXPropertyType_Date
33+
dateNano = OBXPropertyType_DateNano
3334
# relation = OBXPropertyType_Relation
3435
byteVector = OBXPropertyType_ByteVector
3536
intVector = OBXPropertyType_IntVector
@@ -49,7 +50,8 @@ class PropertyType(IntEnum):
4950
PropertyType.float: flatbuffers.number_types.Float32Flags,
5051
PropertyType.double: flatbuffers.number_types.Float64Flags,
5152
PropertyType.string: flatbuffers.number_types.UOffsetTFlags,
52-
# PropertyType.date: flatbuffers.number_types.Int64Flags,
53+
PropertyType.date: flatbuffers.number_types.Int64Flags,
54+
PropertyType.dateNano: flatbuffers.number_types.Int64Flags,
5355
# PropertyType.relation: flatbuffers.number_types.Int64Flags,
5456
PropertyType.byteVector: flatbuffers.number_types.UOffsetTFlags,
5557
PropertyType.intVector: flatbuffers.number_types.UOffsetTFlags,

objectbox/objectbox.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2019-2021 ObjectBox Ltd. All rights reserved.
1+
# Copyright 2019-2023 ObjectBox Ltd. All rights reserved.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -22,10 +22,16 @@ def __init__(self, c_store: OBX_store_p):
2222
self._c_store = c_store
2323

2424
def __del__(self):
25-
obx_store_close(self._c_store)
25+
self.close()
2626

2727
def read_tx(self):
2828
return objectbox.transaction.read(self)
2929

3030
def write_tx(self):
3131
return objectbox.transaction.write(self)
32+
33+
def close(self):
34+
c_store_to_close = self._c_store
35+
if c_store_to_close:
36+
self._c_store = None
37+
obx_store_close(c_store_to_close)

tests/common.py

+22-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import shutil
44
import pytest
5-
from tests.model import TestEntity
5+
from tests.model import TestEntity, TestEntityDatetime
66
import numpy as np
77

88
test_dir = 'testdata'
@@ -24,8 +24,19 @@ def autocleanup():
2424
def load_empty_test_objectbox(name: str = "") -> objectbox.ObjectBox:
2525
model = objectbox.Model()
2626
from objectbox.model import IdUid
27-
model.entity(TestEntity, last_property_id=IdUid(18, 1018))
28-
model.last_entity_id = IdUid(1, 1)
27+
model.entity(TestEntity, last_property_id=IdUid(20, 1020))
28+
model.last_entity_id = IdUid(2, 2)
29+
30+
db_name = test_dir if len(name) == 0 else test_dir + "/" + name
31+
32+
return objectbox.Builder().model(model).directory(db_name).build()
33+
34+
35+
def load_empty_test_datetime(name: str = "") -> objectbox.ObjectBox:
36+
model = objectbox.Model()
37+
from objectbox.model import IdUid
38+
model.entity(TestEntityDatetime, last_property_id=IdUid(3, 2003))
39+
model.last_entity_id = IdUid(2, 2)
2940

3041
db_name = test_dir if len(name) == 0 else test_dir + "/" + name
3142

@@ -40,8 +51,8 @@ def assert_equal_prop_vector(actual, expected, default):
4051
assert (actual == np.array(expected)).all() or (isinstance(
4152
expected, objectbox.model.Property) and actual == default)
4253

43-
# compare approx values for list
44-
def assert_equal_prop_list(actual, expected, default):
54+
# compare approx values
55+
def assert_equal_prop_approx(actual, expected, default):
4556
assert pytest.approx(actual) == expected or (isinstance(
4657
expected, objectbox.model.Property) and actual == default)
4758

@@ -59,7 +70,9 @@ def assert_equal(actual: TestEntity, expected: TestEntity):
5970
assert_equal_prop_vector(actual.longs, expected.longs, np.array([]))
6071
assert_equal_prop_vector(actual.floats, expected.floats, np.array([]))
6172
assert_equal_prop_vector(actual.doubles, expected.doubles, np.array([]))
62-
assert_equal_prop_list(actual.ints_list, expected.ints_list, [])
63-
assert_equal_prop_list(actual.longs_list, expected.longs_list, [])
64-
assert_equal_prop_list(actual.floats_list, expected.floats_list, [])
65-
assert_equal_prop_list(actual.doubles_list, expected.doubles_list, [])
73+
assert_equal_prop_approx(actual.ints_list, expected.ints_list, [])
74+
assert_equal_prop_approx(actual.longs_list, expected.longs_list, [])
75+
assert_equal_prop_approx(actual.floats_list, expected.floats_list, [])
76+
assert_equal_prop_approx(actual.doubles_list, expected.doubles_list, [])
77+
assert_equal_prop_approx(actual.date, expected.date, 0)
78+
assert_equal_prop(actual.date_nano, expected.date_nano, 0)

tests/model.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from objectbox.model import *
22
import numpy as np
3+
from datetime import datetime
34

45

56
@Entity(id=1, uid=1)
@@ -22,7 +23,18 @@ class TestEntity:
2223
longs_list = Property(list, type=PropertyType.longVector, id=16, uid=1016)
2324
floats_list = Property(list, type=PropertyType.floatVector, id=17, uid=1017)
2425
doubles_list = Property(list, type=PropertyType.doubleVector, id=18, uid=1018)
26+
date = Property(int, type=PropertyType.date, id=19, uid=1019)
27+
date_nano = Property(int, type=PropertyType.dateNano, id=20, uid=1020)
2528
transient = "" # not "Property" so it's not stored
2629

2730
def __init__(self, string: str = ""):
2831
self.str = string
32+
33+
@Entity(id=2, uid=2)
34+
class TestEntityDatetime:
35+
id = Id(id=1, uid=2001)
36+
date = Property(datetime, type=PropertyType.date, id=2, uid=2002)
37+
date_nano = Property(datetime, type=PropertyType.dateNano, id=3, uid=2003)
38+
39+
def __init__(self, string: str = ""):
40+
self.str = string

tests/test_basics.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import objectbox
2-
from tests.common import autocleanup, load_empty_test_objectbox, assert_equal
2+
from tests.common import load_empty_test_objectbox
33

44

55
def test_version():

tests/test_box.py

+72-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import pytest
22
import objectbox
3+
from tests.model import TestEntity, TestEntityDatetime
4+
from tests.common import (
5+
autocleanup,
6+
load_empty_test_objectbox,
7+
load_empty_test_datetime,
8+
assert_equal,
9+
)
310
import numpy as np
4-
from tests.model import TestEntity
5-
from tests.common import autocleanup, load_empty_test_objectbox, assert_equal
11+
from datetime import datetime
12+
import time
13+
from math import floor
614

715

816
def test_box_basics():
@@ -38,6 +46,8 @@ def test_box_basics():
3846
object.longs_list = [4568, 8714, 1234, 5678, 9012240941]
3947
object.floats_list = [0.11, 1.22, 2.33, 3.44, 4.5595]
4048
object.doubles_list = [99.1999, 88.2888, 77.3777, 66.4666, 55.6597555]
49+
object.date = time.time() * 1000 # milliseconds since UNIX epoch
50+
object.date_nano = time.time_ns() # nanoseconds since UNIX epoch
4151
object.transient = "abcd"
4252

4353
id = box.put(object)
@@ -54,6 +64,8 @@ def test_box_basics():
5464

5565
# update
5666
object.str = "bar"
67+
object.date = floor(time.time_ns() / 1000000) # check that date can also be int
68+
object.date_nano = float(time.time() * 1000000000) # check that date_nano can also be float
5769
id = box.put(object)
5870
assert id == 5
5971

@@ -72,6 +84,8 @@ def test_box_basics():
7284
with pytest.raises(objectbox.NotFoundException):
7385
box.get(1)
7486

87+
ob.close()
88+
7589

7690
def test_box_bulk():
7791
ob = load_empty_test_objectbox()
@@ -104,3 +118,59 @@ def test_box_bulk():
104118
removed = box.remove_all()
105119
assert removed == 4
106120
assert box.count() == 0
121+
122+
ob.close()
123+
124+
125+
def test_datetime():
126+
ob = load_empty_test_datetime()
127+
box = objectbox.Box(ob, TestEntityDatetime)
128+
129+
assert box.is_empty()
130+
assert box.count() == 0
131+
132+
# creat - deferred for now, as there is an issue with 0 timestamp on Windows
133+
# object = TestEntityDatetime()
134+
# id = box.put(object)
135+
# assert id == 1
136+
# assert id == object.id
137+
138+
# create with a given ID and some data
139+
object = TestEntityDatetime()
140+
object.id = 5
141+
object.date = datetime.utcnow() # milliseconds since UNIX epoch
142+
object.date_nano = datetime.utcnow() # nanoseconds since UNIX epoch
143+
144+
id = box.put(object)
145+
assert id == 5
146+
assert id == object.id
147+
# check the count
148+
assert not box.is_empty()
149+
assert box.count() == 1
150+
151+
# read
152+
read = box.get(object.id)
153+
assert pytest.approx(read.date.timestamp()) == object.date.timestamp()
154+
155+
# update
156+
object.str = "bar"
157+
object.date = datetime.utcnow()
158+
object.date_nano = datetime.utcnow()
159+
id = box.put(object)
160+
assert id == 5
161+
162+
# read again
163+
read = box.get(object.id)
164+
assert pytest.approx(read.date.timestamp()) == object.date.timestamp()
165+
166+
# remove
167+
box.remove(object)
168+
169+
# check they're gone
170+
assert box.count() == 0
171+
with pytest.raises(objectbox.NotFoundException):
172+
box.get(object.id)
173+
with pytest.raises(objectbox.NotFoundException):
174+
box.get(1)
175+
176+
ob.close()

tests/test_transactions.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import pytest
21
import objectbox
32
from tests.model import TestEntity
4-
from tests.common import autocleanup, load_empty_test_objectbox, assert_equal
3+
from tests.common import autocleanup, load_empty_test_objectbox
54

65

76
def test_transactions():
@@ -39,3 +38,5 @@ def test_transactions():
3938
assert 0
4039
except Exception as err:
4140
assert "Cannot start a write transaction inside a read only transaction" in str(err)
41+
finally:
42+
ob.close()

0 commit comments

Comments
 (0)