Skip to content

Commit

Permalink
Add the from_time query parameter to the live tracking endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb committed Dec 22, 2020
1 parent 5e9a31b commit 814a85c
Show file tree
Hide file tree
Showing 11 changed files with 677 additions and 202 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ jobs:
python-version: 3.6

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

deploy:
name: Deploy
Expand Down
2 changes: 1 addition & 1 deletion skylines/api/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def tokengetter(access_token=None, refresh_token=None):

@staticmethod
def tokensetter(token, request, *args, **kwargs):
""" Save a new token to the database.
"""Save a new token to the database.
:param token: Token dictionary containing access and refresh tokens, plus token type.
:param request: Request dictionary containing information about the client and user.
Expand Down
49 changes: 42 additions & 7 deletions skylines/api/views/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_nearest_airport(track):
)

tracks = []
for t in TrackingFix.get_latest():
for t in TrackingFix.get_from_time():
nearest_airport = get_nearest_airport(t)

track = fix_schema.dump(t).data
Expand All @@ -66,8 +66,17 @@ def get_nearest_airport(track):
@tracking_blueprint.route("/tracking/latest.json")
@jsonp
def latest():
"""
Supported query parameter:
- from_time: Returns only the fixes after `from_time` expressed as a UNIX
timestamp. The maximum age of the returned fixes is 6h.
"""
fixes = []
for fix in TrackingFix.get_latest():
from_time = request.values.get("from_time", 0, type=int)

for fix in TrackingFix.get_from_time(
from_time=from_time, max_age=timedelta(hours=6)
):
json = dict(
time=fix.time.isoformat() + "Z",
location=fix.location.to_wkt(),
Expand Down Expand Up @@ -95,13 +104,22 @@ def latest():
@tracking_blueprint.route("/tracking/<user_ids>", strict_slashes=False)
@tracking_blueprint.route("/live/<user_ids>", strict_slashes=False)
def read(user_ids):
"""
Supported query parameter:
- from_time: Returns only the fixes after `from_time` expressed as a UNIX
timestamp. The maximum age of the fix is 12 hours.
"""
from_time = request.values.get("from_time", 0, type=int)

pilots = get_requested_record_list(User, user_ids, joinedload=[User.club])

color_gen = color.generator()
for pilot in pilots:
pilot.color = next(color_gen)

traces = list(map(_get_flight_path, pilots))
traces = list(
map(lambda pilot: _get_flight_path(pilot, from_time=from_time), pilots)
)
if not any(traces):
traces = None

Expand Down Expand Up @@ -142,10 +160,23 @@ def read(user_ids):
@tracking_blueprint.route("/tracking/<user_id>/json")
@tracking_blueprint.route("/live/<user_id>/json")
def json(user_id):
"""
Supported query parameters:
- last_update: Returns only the fixes after the `last_update` expressed in
seconds from the first fix,
- from_time: Returns only the fixes after `from_time` expressed as a UNIX
timestamp.
Specifying both parameters is equivalent to ANDing the conditions.
The maximum age of the fixes is 12h.
"""
pilot = get_requested_record(User, user_id, joinedload=[User.club])
last_update = request.values.get("last_update", 0, type=int)
from_time = request.values.get("from_time", 0, type=int)

trace = _get_flight_path(pilot, threshold=0.001, last_update=last_update)
trace = _get_flight_path(
pilot, threshold=0.001, last_update=last_update, from_time=from_time
)
if not trace:
abort(404)

Expand All @@ -160,8 +191,8 @@ def json(user_id):
)


def _get_flight_path(pilot, threshold=0.001, last_update=None):
fp = _get_flight_path2(pilot, last_update=last_update)
def _get_flight_path(pilot, threshold=0.001, last_update=None, from_time=None):
fp = _get_flight_path2(pilot, last_update=last_update, from_time=from_time)
if not fp:
return None

Expand Down Expand Up @@ -217,7 +248,7 @@ def _get_flight_path(pilot, threshold=0.001, last_update=None):
)


def _get_flight_path2(pilot, last_update=None):
def _get_flight_path2(pilot, last_update=None, from_time=None):
query = TrackingFix.query().filter(
and_(
TrackingFix.pilot == pilot,
Expand Down Expand Up @@ -245,6 +276,10 @@ def _get_flight_path2(pilot, last_update=None):
>= start_fix.time + timedelta(seconds=(last_update - start_time))
)

if from_time:
from_datetime_utc = datetime.utcfromtimestamp(from_time)
query = query.filter(TrackingFix.time >= from_datetime_utc)

result = []
for fix in query:
location = fix.location
Expand Down
4 changes: 2 additions & 2 deletions skylines/lib/igc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@


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

if is_string(f):
try:
Expand Down
21 changes: 18 additions & 3 deletions skylines/model/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def max_age_filter(cls, max_age):
Returns a filter that makes sure that the fix is not older than a
certain time.
The delay parameter can be either a datetime.timedelta or a numeric
The max_age parameter can be either a datetime.timedelta or a numeric
value that will be interpreted as hours.
"""

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

@classmethod
def get_latest(cls, max_age=timedelta(hours=6)):
def get_from_time(cls, from_time=0, max_age=timedelta(hours=6)):
"""
Creates a query returning fixes from the timestamp from_time having a
maximum age of max_age.
The max_age parameter can be either a datetime.timedelta or a numeric
value that will be interpreted as hours.
"""
if is_int(max_age) or isinstance(max_age, float):
max_age = timedelta(hours=max_age)

# from_time is only taken into account if more recent than max_age.
from_datetime_utc = datetime.utcfromtimestamp(from_time)
age = datetime.utcnow() - from_datetime_utc
age_filter = TrackingFix.max_age_filter(min(age, max_age))

# Add a db.Column to the inner query with
# numbers ordered by time for each pilot
row_number = db.over(
Expand All @@ -92,7 +107,7 @@ def get_latest(cls, max_age=timedelta(hours=6)):
subq = (
db.session.query(cls.id, row_number.label("row_number"))
.join(cls.pilot)
.filter(cls.max_age_filter(max_age))
.filter(age_filter)
.filter(cls.time_visible <= datetime.utcnow())
.filter(cls.location_wkt != None)
.subquery()
Expand Down
7 changes: 7 additions & 0 deletions tests/api/views/tracking/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from time import mktime


def decode_time(encoded_time):
"""Decodes an encoded time string"""
index = 0
Expand Down Expand Up @@ -34,3 +37,7 @@ def get_fixes_times_seconds(fixes):
seconds.append(int((time - start_time).total_seconds() + start_second_of_day))

return seconds


def to_timestamp(dtime):
return int(mktime(dtime.timetuple()))
90 changes: 90 additions & 0 deletions tests/api/views/tracking/latest_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from mock import patch
from datetime import datetime, timedelta
from tests.data import add_fixtures, users, live_fix
from tests.api.views.tracking import to_timestamp


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

with patch("skylines.model.tracking.datetime") as datetime_mock:
datetime_mock.utcfromtimestamp.side_effect = (
lambda *args, **kw: datetime.utcfromtimestamp(*args, **kw)
)
datetime_mock.utcnow.return_value = utcnow

res = client.get("/tracking/latest.json")
Expand All @@ -38,3 +42,89 @@ def test_get_latest_default_max_age(db_session, client):
}
]
}


def test_get_latest_filtered_by_from_time(db_session, client):
utcnow = datetime(year=2020, month=12, day=20, hour=12)
from_time = utcnow - timedelta(minutes=5)

# This fix datetime is from_time, it should be returned
john = users.john()
fix_john = live_fix.create(john, from_time, 11, 21)

# This fix is before from_time and should not be returned
jane = users.jane()
fix_jane = live_fix.create(jane, from_time - timedelta(minutes=10), 12, 22)

add_fixtures(db_session, john, fix_john, jane, fix_jane)

with patch("skylines.model.tracking.datetime") as datetime_mock:
datetime_mock.utcfromtimestamp.side_effect = (
lambda *args, **kw: datetime.utcfromtimestamp(*args, **kw)
)
datetime_mock.utcnow.return_value = utcnow

res = client.get(
"/tracking/latest.json?from_time={from_time}".format(
from_time=to_timestamp(from_time)
)
)

assert res.status_code == 200
assert res.json == {
u"fixes": [
{
u"airspeed": 10,
u"altitude": 100,
u"ground_speed": 10,
u"location": u"POINT(11.0 21.0)",
u"pilot": {u"id": john.id, u"name": u"John Doe"},
u"time": u"2020-12-20T11:55:00Z",
u"track": 0,
u"vario": 0,
}
]
}


def test_get_from_time_max_6h(db_session, client):
utcnow = datetime(year=2020, month=12, day=20, hour=12)
from_time = utcnow - timedelta(hours=7)

# This fix age is 7h, it should not be returned
john = users.john()
fix_john = live_fix.create(john, from_time, 11, 21)

# This fix age is 10mn, it should be returned
jane = users.jane()
fix_jane = live_fix.create(jane, utcnow - timedelta(minutes=10), 12, 22)

add_fixtures(db_session, john, fix_john, jane, fix_jane)

with patch("skylines.model.tracking.datetime") as datetime_mock:
datetime_mock.utcfromtimestamp.side_effect = (
lambda *args, **kw: datetime.utcfromtimestamp(*args, **kw)
)
datetime_mock.utcnow.return_value = utcnow

res = client.get(
"/tracking/latest.json?from_time={from_time}".format(
from_time=to_timestamp(from_time)
)
)

assert res.status_code == 200
assert res.json == {
u"fixes": [
{
u"airspeed": 10,
u"altitude": 100,
u"ground_speed": 10,
u"location": u"POINT(12.0 22.0)",
u"pilot": {u"id": jane.id, u"name": u"Jane Doe"},
u"time": u"2020-12-20T11:50:00Z",
u"track": 0,
u"vario": 0,
}
]
}
Loading

0 comments on commit 814a85c

Please sign in to comment.