Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the from_time query parameter to the live tracking endpoints #2312

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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