Skip to content

Commit e06b6eb

Browse files
committed
feat: made all queries range inclusive
1 parent f04a450 commit e06b6eb

File tree

4 files changed

+163
-47
lines changed

4 files changed

+163
-47
lines changed

aw_datastore/storages/memory.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,16 @@ def get_events(
5757
endtime: datetime = None,
5858
) -> List[Event]:
5959
events = self.db[bucket]
60+
6061
# Sort by timestamp
6162
events = sorted(events, key=lambda k: k["timestamp"])[::-1]
63+
6264
# Filter by date
6365
if starttime:
64-
e = []
65-
for event in events:
66-
if event.timestamp >= starttime:
67-
e.append(event)
68-
events = e
66+
events = [e for e in events if starttime <= (e.timestamp + e.duration)]
6967
if endtime:
70-
e = []
71-
for event in events:
72-
if event.timestamp <= endtime:
73-
e.append(event)
74-
events = e
68+
events = [e for e in events if e.timestamp <= endtime]
69+
7570
# Limit
7671
if limit == 0:
7772
return []

aw_datastore/storages/peewee.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import iso8601
77

8+
import peewee
89
from peewee import (
910
Model,
1011
CharField,
@@ -44,6 +45,14 @@ def chunks(l, n):
4445
yield l[i : i + n]
4546

4647

48+
def dt_plus_duration(dt, duration):
49+
return peewee.fn.strftime(
50+
"%Y-%m-%d %H:%M:%f+00:00",
51+
(peewee.fn.julianday(dt) - 2440587.5) * 86400.0 + duration,
52+
"unixepoch",
53+
)
54+
55+
4756
class BaseModel(Model):
4857
class Meta:
4958
database = _db
@@ -240,6 +249,21 @@ def get_events(
240249
starttime: Optional[datetime] = None,
241250
endtime: Optional[datetime] = None,
242251
):
252+
"""
253+
Fetch events from a certain bucket, optionally from a given range of time.
254+
255+
Example raw query:
256+
257+
SELECT strftime(
258+
"%Y-%m-%d %H:%M:%f",
259+
((julianday(timestamp) - 2440587.5) * 86400),
260+
'unixepoch'
261+
)
262+
FROM eventmodel
263+
WHERE eventmodel.timestamp > '2021-06-20'
264+
LIMIT 10;
265+
266+
"""
243267
if limit == 0:
244268
return []
245269
q = (
@@ -248,27 +272,40 @@ def get_events(
248272
.order_by(EventModel.timestamp.desc())
249273
.limit(limit)
250274
)
251-
if starttime:
252-
# Important to normalize datetimes to UTC, otherwise any UTC offset will be ignored
253-
starttime = starttime.astimezone(timezone.utc)
254-
q = q.where(starttime <= EventModel.timestamp)
255-
if endtime:
256-
endtime = endtime.astimezone(timezone.utc)
257-
q = q.where(EventModel.timestamp <= endtime)
275+
276+
# See peewee docs on datemath: https://docs.peewee-orm.com/en/latest/peewee/hacks.html#date-math
277+
logging.getLogger("peewee").setLevel(logging.DEBUG)
278+
279+
q = self._where_range(q, starttime, endtime)
258280
return [Event(**e) for e in list(map(EventModel.json, q.execute()))]
259281

260282
def get_eventcount(
261283
self,
262284
bucket_id: str,
263285
starttime: Optional[datetime] = None,
264286
endtime: Optional[datetime] = None,
265-
):
287+
) -> int:
266288
q = EventModel.select().where(EventModel.bucket == self.bucket_keys[bucket_id])
289+
q = self._where_range(q, starttime, endtime)
290+
return q.count()
291+
292+
def _where_range(
293+
self,
294+
q,
295+
starttime: Optional[datetime] = None,
296+
endtime: Optional[datetime] = None,
297+
):
298+
# Important to normalize datetimes to UTC, otherwise any UTC offset will be ignored
267299
if starttime:
268-
# Important to normalize datetimes to UTC, otherwise any UTC offset will be ignored
269300
starttime = starttime.astimezone(timezone.utc)
270-
q = q.where(starttime <= EventModel.timestamp)
271301
if endtime:
272302
endtime = endtime.astimezone(timezone.utc)
303+
304+
if starttime:
305+
q = q.where(
306+
starttime <= dt_plus_duration(EventModel.timestamp, EventModel.duration)
307+
)
308+
if endtime:
273309
q = q.where(EventModel.timestamp <= endtime)
274-
return q.count()
310+
311+
return q

aw_datastore/storages/sqlite.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,13 @@ def get_events(
263263
c = self.conn.cursor()
264264
starttime_i = starttime.timestamp() * 1000000 if starttime else 0
265265
endtime_i = endtime.timestamp() * 1000000 if endtime else MAX_TIMESTAMP
266-
query = (
267-
"SELECT id, starttime, endtime, datastr "
268-
+ "FROM events "
269-
+ "WHERE bucketrow = (SELECT rowid FROM buckets WHERE id = ?) "
270-
+ "AND starttime >= ? AND endtime <= ? "
271-
+ "ORDER BY endtime DESC LIMIT ?"
272-
)
266+
query = """
267+
SELECT id, starttime, endtime, datastr
268+
FROM events
269+
WHERE bucketrow = (SELECT rowid FROM buckets WHERE id = ?)
270+
AND endtime >= ? AND starttime <= ?
271+
ORDER BY endtime DESC LIMIT ?
272+
"""
273273
rows = c.execute(query, [bucket_id, starttime_i, endtime_i, limit])
274274
events = []
275275
for row in rows:

tests/test_datastore.py

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
logging.basicConfig(level=logging.DEBUG)
1818

1919
# Useful when you just want some placeholder time in your events, saves typing
20-
now = datetime.now(timezone.utc)
20+
now = datetime.now(tz=timezone.utc)
21+
td1s = timedelta(seconds=1)
2122

2223

2324
def test_get_storage_methods():
@@ -84,11 +85,11 @@ def test_insert_one(bucket_cm):
8485
Tests inserting one event into a bucket
8586
"""
8687
with bucket_cm as bucket:
87-
l = len(bucket.get())
88-
event = Event(timestamp=now, duration=timedelta(seconds=1), data={"key": "val"})
88+
n_events = len(bucket.get())
89+
event = Event(timestamp=now, duration=td1s, data={"key": "val"})
8990
bucket.insert(event)
9091
fetched_events = bucket.get()
91-
assert l + 1 == len(fetched_events)
92+
assert n_events + 1 == len(fetched_events)
9293
assert isinstance(fetched_events[0], Event)
9394
assert event == fetched_events[0]
9495
logging.info(event)
@@ -111,9 +112,7 @@ def test_insert_many(bucket_cm):
111112
"""
112113
num_events = 5000
113114
with bucket_cm as bucket:
114-
events = num_events * [
115-
Event(timestamp=now, duration=timedelta(seconds=1), data={"key": "val"})
116-
]
115+
events = num_events * [Event(timestamp=now, duration=td1s, data={"key": "val"})]
117116
bucket.insert(events)
118117
fetched_events = bucket.get(limit=-1)
119118
assert num_events == len(fetched_events)
@@ -128,9 +127,7 @@ def test_delete(bucket_cm):
128127
"""
129128
num_events = 10
130129
with bucket_cm as bucket:
131-
events = num_events * [
132-
Event(timestamp=now, duration=timedelta(seconds=1), data={"key": "val"})
133-
]
130+
events = num_events * [Event(timestamp=now, duration=td1s, data={"key": "val"})]
134131
bucket.insert(events)
135132

136133
fetched_events = bucket.get(limit=-1)
@@ -170,7 +167,7 @@ def test_get_ordered(bucket_cm):
170167
eventcount = 10
171168
events = []
172169
for i in range(10):
173-
events.append(Event(timestamp=now + timedelta(seconds=i)))
170+
events.append(Event(timestamp=now + i * td1s, duration=td1s))
174171
random.shuffle(events)
175172
print(events)
176173
bucket.insert(events)
@@ -205,32 +202,119 @@ def test_get_event_with_timezone(bucket_cm):
205202

206203

207204
@pytest.mark.parametrize("bucket_cm", param_testing_buckets_cm())
208-
def test_get_datefilter(bucket_cm):
205+
def test_get_datefilter_simple(bucket_cm):
206+
with bucket_cm as bucket:
207+
eventcount = 3
208+
events = [
209+
Event(timestamp=now + i * td1s, duration=td1s) for i in range(eventcount)
210+
]
211+
bucket.insert(events)
212+
213+
# Get first event, but expect only half the event to match the interval
214+
fetched_events = bucket.get(
215+
-1,
216+
starttime=now - 0.5 * td1s,
217+
endtime=now + 0.5 * td1s,
218+
)
219+
assert 1 == len(fetched_events)
220+
221+
# Get first two events, but expect only half of each to match the interval
222+
fetched_events = bucket.get(
223+
-1,
224+
starttime=now + 0.5 * td1s,
225+
endtime=now + 1.5 * td1s,
226+
)
227+
assert 2 == len(fetched_events)
228+
229+
# Get last event, but expect only half to match the interval
230+
fetched_events = bucket.get(
231+
-1,
232+
starttime=now + 2.5 * td1s,
233+
endtime=now + 3.5 * td1s,
234+
)
235+
assert 1 == len(fetched_events)
236+
237+
# Check approx precision
238+
fetched_events = bucket.get(
239+
-1,
240+
starttime=now - 0.01 * td1s,
241+
endtime=now + 0.01 * td1s,
242+
)
243+
assert 1 == len(fetched_events)
244+
245+
# Check precision of start
246+
fetched_events = bucket.get(
247+
-1,
248+
starttime=now,
249+
endtime=now,
250+
)
251+
assert 1 == len(fetched_events)
252+
253+
# Check approx precision of end
254+
fetched_events = bucket.get(
255+
-1,
256+
starttime=now + 2.99 * td1s,
257+
endtime=now + 3.01 * td1s,
258+
)
259+
assert 1 == len(fetched_events)
260+
261+
262+
@pytest.mark.parametrize("bucket_cm", param_testing_buckets_cm())
263+
def test_get_datefilter_start(bucket_cm):
209264
"""
210265
Tests the datetimefilter when fetching events
211266
"""
212267
with bucket_cm as bucket:
213268
eventcount = 10
214-
events = []
215-
for i in range(10):
216-
events.append(Event(timestamp=now + timedelta(seconds=i)))
269+
events = [
270+
Event(timestamp=now + i * td1s, duration=td1s) for i in range(eventcount)
271+
]
217272
bucket.insert(events)
218273

219274
# Starttime
220275
for i in range(eventcount):
221-
fetched_events = bucket.get(-1, starttime=events[i].timestamp)
276+
fetched_events = bucket.get(-1, starttime=events[i].timestamp + 0.01 * td1s)
222277
assert eventcount - i == len(fetched_events)
223278

279+
280+
@pytest.mark.parametrize("bucket_cm", param_testing_buckets_cm())
281+
def test_get_datefilter_end(bucket_cm):
282+
"""
283+
Tests the datetimefilter when fetching events
284+
"""
285+
with bucket_cm as bucket:
286+
eventcount = 10
287+
events = [
288+
Event(timestamp=now + i * td1s, duration=td1s) for i in range(eventcount)
289+
]
290+
bucket.insert(events)
291+
224292
# Endtime
225293
for i in range(eventcount):
226-
fetched_events = bucket.get(-1, endtime=events[i].timestamp)
227-
assert i + 1 == len(fetched_events)
294+
fetched_events = bucket.get(-1, endtime=events[i].timestamp - 0.01 * td1s)
295+
assert i == len(fetched_events)
296+
297+
298+
@pytest.mark.parametrize("bucket_cm", param_testing_buckets_cm())
299+
def test_get_datefilter_both(bucket_cm):
300+
"""
301+
Tests the datetimefilter when fetching events
302+
"""
303+
with bucket_cm as bucket:
304+
eventcount = 10
305+
events = [
306+
Event(timestamp=now + i * td1s, duration=td1s) for i in range(eventcount)
307+
]
308+
bucket.insert(events)
228309

229310
# Both
230311
for i in range(eventcount):
231312
for j in range(i + 1, eventcount):
232313
fetched_events = bucket.get(
233-
starttime=events[i].timestamp, endtime=events[j].timestamp
314+
starttime=events[i].timestamp + timedelta(seconds=0.01),
315+
endtime=events[j].timestamp
316+
+ events[j].duration
317+
- timedelta(seconds=0.01),
234318
)
235319
assert j - i + 1 == len(fetched_events)
236320

0 commit comments

Comments
 (0)