Skip to content

Commit 814a85c

Browse files
committed
Add the from_time query parameter to the live tracking endpoints
1 parent 5e9a31b commit 814a85c

File tree

11 files changed

+677
-202
lines changed

11 files changed

+677
-202
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
python-version: 3.6
9595

9696
- run: pip install black==18.9b0
97-
- run: black config migrations skylines tests *.py --check
97+
- run: black config migrations skylines tests *.py --check --diff
9898

9999
deploy:
100100
name: Deploy

skylines/api/oauth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def tokengetter(access_token=None, refresh_token=None):
119119

120120
@staticmethod
121121
def tokensetter(token, request, *args, **kwargs):
122-
""" Save a new token to the database.
122+
"""Save a new token to the database.
123123
124124
:param token: Token dictionary containing access and refresh tokens, plus token type.
125125
:param request: Request dictionary containing information about the client and user.

skylines/api/views/tracking.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def get_nearest_airport(track):
4343
)
4444

4545
tracks = []
46-
for t in TrackingFix.get_latest():
46+
for t in TrackingFix.get_from_time():
4747
nearest_airport = get_nearest_airport(t)
4848

4949
track = fix_schema.dump(t).data
@@ -66,8 +66,17 @@ def get_nearest_airport(track):
6666
@tracking_blueprint.route("/tracking/latest.json")
6767
@jsonp
6868
def latest():
69+
"""
70+
Supported query parameter:
71+
- from_time: Returns only the fixes after `from_time` expressed as a UNIX
72+
timestamp. The maximum age of the returned fixes is 6h.
73+
"""
6974
fixes = []
70-
for fix in TrackingFix.get_latest():
75+
from_time = request.values.get("from_time", 0, type=int)
76+
77+
for fix in TrackingFix.get_from_time(
78+
from_time=from_time, max_age=timedelta(hours=6)
79+
):
7180
json = dict(
7281
time=fix.time.isoformat() + "Z",
7382
location=fix.location.to_wkt(),
@@ -95,13 +104,22 @@ def latest():
95104
@tracking_blueprint.route("/tracking/<user_ids>", strict_slashes=False)
96105
@tracking_blueprint.route("/live/<user_ids>", strict_slashes=False)
97106
def read(user_ids):
107+
"""
108+
Supported query parameter:
109+
- from_time: Returns only the fixes after `from_time` expressed as a UNIX
110+
timestamp. The maximum age of the fix is 12 hours.
111+
"""
112+
from_time = request.values.get("from_time", 0, type=int)
113+
98114
pilots = get_requested_record_list(User, user_ids, joinedload=[User.club])
99115

100116
color_gen = color.generator()
101117
for pilot in pilots:
102118
pilot.color = next(color_gen)
103119

104-
traces = list(map(_get_flight_path, pilots))
120+
traces = list(
121+
map(lambda pilot: _get_flight_path(pilot, from_time=from_time), pilots)
122+
)
105123
if not any(traces):
106124
traces = None
107125

@@ -142,10 +160,23 @@ def read(user_ids):
142160
@tracking_blueprint.route("/tracking/<user_id>/json")
143161
@tracking_blueprint.route("/live/<user_id>/json")
144162
def json(user_id):
163+
"""
164+
Supported query parameters:
165+
- last_update: Returns only the fixes after the `last_update` expressed in
166+
seconds from the first fix,
167+
- from_time: Returns only the fixes after `from_time` expressed as a UNIX
168+
timestamp.
169+
170+
Specifying both parameters is equivalent to ANDing the conditions.
171+
The maximum age of the fixes is 12h.
172+
"""
145173
pilot = get_requested_record(User, user_id, joinedload=[User.club])
146174
last_update = request.values.get("last_update", 0, type=int)
175+
from_time = request.values.get("from_time", 0, type=int)
147176

148-
trace = _get_flight_path(pilot, threshold=0.001, last_update=last_update)
177+
trace = _get_flight_path(
178+
pilot, threshold=0.001, last_update=last_update, from_time=from_time
179+
)
149180
if not trace:
150181
abort(404)
151182

@@ -160,8 +191,8 @@ def json(user_id):
160191
)
161192

162193

163-
def _get_flight_path(pilot, threshold=0.001, last_update=None):
164-
fp = _get_flight_path2(pilot, last_update=last_update)
194+
def _get_flight_path(pilot, threshold=0.001, last_update=None, from_time=None):
195+
fp = _get_flight_path2(pilot, last_update=last_update, from_time=from_time)
165196
if not fp:
166197
return None
167198

@@ -217,7 +248,7 @@ def _get_flight_path(pilot, threshold=0.001, last_update=None):
217248
)
218249

219250

220-
def _get_flight_path2(pilot, last_update=None):
251+
def _get_flight_path2(pilot, last_update=None, from_time=None):
221252
query = TrackingFix.query().filter(
222253
and_(
223254
TrackingFix.pilot == pilot,
@@ -245,6 +276,10 @@ def _get_flight_path2(pilot, last_update=None):
245276
>= start_fix.time + timedelta(seconds=(last_update - start_time))
246277
)
247278

279+
if from_time:
280+
from_datetime_utc = datetime.utcfromtimestamp(from_time)
281+
query = query.filter(TrackingFix.time >= from_datetime_utc)
282+
248283
result = []
249284
for fix in query:
250285
location = fix.location

skylines/lib/igc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818

1919
def read_igc_headers(f):
20-
""" Read IGC file headers from a file-like object, a list of strings or a
21-
file if the parameter is a path. """
20+
"""Read IGC file headers from a file-like object, a list of strings or a
21+
file if the parameter is a path."""
2222

2323
if is_string(f):
2424
try:

skylines/model/tracking.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def max_age_filter(cls, max_age):
7171
Returns a filter that makes sure that the fix is not older than a
7272
certain time.
7373
74-
The delay parameter can be either a datetime.timedelta or a numeric
74+
The max_age parameter can be either a datetime.timedelta or a numeric
7575
value that will be interpreted as hours.
7676
"""
7777

@@ -81,7 +81,22 @@ def max_age_filter(cls, max_age):
8181
return cls.time >= datetime.utcnow() - max_age
8282

8383
@classmethod
84-
def get_latest(cls, max_age=timedelta(hours=6)):
84+
def get_from_time(cls, from_time=0, max_age=timedelta(hours=6)):
85+
"""
86+
Creates a query returning fixes from the timestamp from_time having a
87+
maximum age of max_age.
88+
89+
The max_age parameter can be either a datetime.timedelta or a numeric
90+
value that will be interpreted as hours.
91+
"""
92+
if is_int(max_age) or isinstance(max_age, float):
93+
max_age = timedelta(hours=max_age)
94+
95+
# from_time is only taken into account if more recent than max_age.
96+
from_datetime_utc = datetime.utcfromtimestamp(from_time)
97+
age = datetime.utcnow() - from_datetime_utc
98+
age_filter = TrackingFix.max_age_filter(min(age, max_age))
99+
85100
# Add a db.Column to the inner query with
86101
# numbers ordered by time for each pilot
87102
row_number = db.over(
@@ -92,7 +107,7 @@ def get_latest(cls, max_age=timedelta(hours=6)):
92107
subq = (
93108
db.session.query(cls.id, row_number.label("row_number"))
94109
.join(cls.pilot)
95-
.filter(cls.max_age_filter(max_age))
110+
.filter(age_filter)
96111
.filter(cls.time_visible <= datetime.utcnow())
97112
.filter(cls.location_wkt != None)
98113
.subquery()

tests/api/views/tracking/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from time import mktime
2+
3+
14
def decode_time(encoded_time):
25
"""Decodes an encoded time string"""
36
index = 0
@@ -34,3 +37,7 @@ def get_fixes_times_seconds(fixes):
3437
seconds.append(int((time - start_time).total_seconds() + start_second_of_day))
3538

3639
return seconds
40+
41+
42+
def to_timestamp(dtime):
43+
return int(mktime(dtime.timetuple()))

tests/api/views/tracking/latest_test.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from mock import patch
22
from datetime import datetime, timedelta
33
from tests.data import add_fixtures, users, live_fix
4+
from tests.api.views.tracking import to_timestamp
45

56

67
def test_get_latest_default_max_age(db_session, client):
@@ -19,6 +20,9 @@ def test_get_latest_default_max_age(db_session, client):
1920
add_fixtures(db_session, john, fix, latest_fix, jane, old_fix)
2021

2122
with patch("skylines.model.tracking.datetime") as datetime_mock:
23+
datetime_mock.utcfromtimestamp.side_effect = (
24+
lambda *args, **kw: datetime.utcfromtimestamp(*args, **kw)
25+
)
2226
datetime_mock.utcnow.return_value = utcnow
2327

2428
res = client.get("/tracking/latest.json")
@@ -38,3 +42,89 @@ def test_get_latest_default_max_age(db_session, client):
3842
}
3943
]
4044
}
45+
46+
47+
def test_get_latest_filtered_by_from_time(db_session, client):
48+
utcnow = datetime(year=2020, month=12, day=20, hour=12)
49+
from_time = utcnow - timedelta(minutes=5)
50+
51+
# This fix datetime is from_time, it should be returned
52+
john = users.john()
53+
fix_john = live_fix.create(john, from_time, 11, 21)
54+
55+
# This fix is before from_time and should not be returned
56+
jane = users.jane()
57+
fix_jane = live_fix.create(jane, from_time - timedelta(minutes=10), 12, 22)
58+
59+
add_fixtures(db_session, john, fix_john, jane, fix_jane)
60+
61+
with patch("skylines.model.tracking.datetime") as datetime_mock:
62+
datetime_mock.utcfromtimestamp.side_effect = (
63+
lambda *args, **kw: datetime.utcfromtimestamp(*args, **kw)
64+
)
65+
datetime_mock.utcnow.return_value = utcnow
66+
67+
res = client.get(
68+
"/tracking/latest.json?from_time={from_time}".format(
69+
from_time=to_timestamp(from_time)
70+
)
71+
)
72+
73+
assert res.status_code == 200
74+
assert res.json == {
75+
u"fixes": [
76+
{
77+
u"airspeed": 10,
78+
u"altitude": 100,
79+
u"ground_speed": 10,
80+
u"location": u"POINT(11.0 21.0)",
81+
u"pilot": {u"id": john.id, u"name": u"John Doe"},
82+
u"time": u"2020-12-20T11:55:00Z",
83+
u"track": 0,
84+
u"vario": 0,
85+
}
86+
]
87+
}
88+
89+
90+
def test_get_from_time_max_6h(db_session, client):
91+
utcnow = datetime(year=2020, month=12, day=20, hour=12)
92+
from_time = utcnow - timedelta(hours=7)
93+
94+
# This fix age is 7h, it should not be returned
95+
john = users.john()
96+
fix_john = live_fix.create(john, from_time, 11, 21)
97+
98+
# This fix age is 10mn, it should be returned
99+
jane = users.jane()
100+
fix_jane = live_fix.create(jane, utcnow - timedelta(minutes=10), 12, 22)
101+
102+
add_fixtures(db_session, john, fix_john, jane, fix_jane)
103+
104+
with patch("skylines.model.tracking.datetime") as datetime_mock:
105+
datetime_mock.utcfromtimestamp.side_effect = (
106+
lambda *args, **kw: datetime.utcfromtimestamp(*args, **kw)
107+
)
108+
datetime_mock.utcnow.return_value = utcnow
109+
110+
res = client.get(
111+
"/tracking/latest.json?from_time={from_time}".format(
112+
from_time=to_timestamp(from_time)
113+
)
114+
)
115+
116+
assert res.status_code == 200
117+
assert res.json == {
118+
u"fixes": [
119+
{
120+
u"airspeed": 10,
121+
u"altitude": 100,
122+
u"ground_speed": 10,
123+
u"location": u"POINT(12.0 22.0)",
124+
u"pilot": {u"id": jane.id, u"name": u"Jane Doe"},
125+
u"time": u"2020-12-20T11:50:00Z",
126+
u"track": 0,
127+
u"vario": 0,
128+
}
129+
]
130+
}

0 commit comments

Comments
 (0)