API - handle gpx files with offset
This commit is contained in:
parent
5874933643
commit
4288c3c387
114
fittrackee/tests/fixtures/fixtures_workouts.py
vendored
114
fittrackee/tests/fixtures/fixtures_workouts.py
vendored
@ -452,6 +452,120 @@ def gpx_file_wo_name() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def gpx_file_with_offset() -> str:
|
||||||
|
return (
|
||||||
|
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||||
|
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||||
|
' <metadata/>'
|
||||||
|
' <trk>'
|
||||||
|
' <trkseg>'
|
||||||
|
' <trkpt lat="44.68095" lon="6.07367">'
|
||||||
|
' <ele>998</ele>'
|
||||||
|
' <time>2018-03-13T13:44:45+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68091" lon="6.07367">'
|
||||||
|
' <ele>998</ele>'
|
||||||
|
' <time>2018-03-13T13:44:50+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.6808" lon="6.07364">'
|
||||||
|
' <ele>994</ele>'
|
||||||
|
' <time>2018-03-13T13:45:00+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68075" lon="6.07364">'
|
||||||
|
' <ele>994</ele>'
|
||||||
|
' <time>2018-03-13T13:45:05+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68071" lon="6.07364">'
|
||||||
|
' <ele>994</ele>'
|
||||||
|
' <time>2018-03-13T13:45:10+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68049" lon="6.07361">'
|
||||||
|
' <ele>993</ele>'
|
||||||
|
' <time>2018-03-13T13:45:30+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68019" lon="6.07356">'
|
||||||
|
' <ele>992</ele>'
|
||||||
|
' <time>2018-03-13T13:45:55+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68014" lon="6.07355">'
|
||||||
|
' <ele>992</ele>'
|
||||||
|
' <time>2018-03-13T13:46:00+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67995" lon="6.07358">'
|
||||||
|
' <ele>987</ele>'
|
||||||
|
' <time>2018-03-13T13:46:15+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67977" lon="6.07364">'
|
||||||
|
' <ele>987</ele>'
|
||||||
|
' <time>2018-03-13T13:46:30+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67972" lon="6.07367">'
|
||||||
|
' <ele>987</ele>'
|
||||||
|
' <time>2018-03-13T13:46:35+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67966" lon="6.07368">'
|
||||||
|
' <ele>987</ele>'
|
||||||
|
' <time>2018-03-13T13:46:40+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67961" lon="6.0737">'
|
||||||
|
' <ele>986</ele>'
|
||||||
|
' <time>2018-03-13T13:46:45+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67938" lon="6.07377">'
|
||||||
|
' <ele>986</ele>'
|
||||||
|
' <time>2018-03-13T13:47:05+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67933" lon="6.07381">'
|
||||||
|
' <ele>986</ele>'
|
||||||
|
' <time>2018-03-13T13:47:10+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67922" lon="6.07385">'
|
||||||
|
' <ele>985</ele>'
|
||||||
|
' <time>2018-03-13T13:47:20+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67911" lon="6.0739">'
|
||||||
|
' <ele>980</ele>'
|
||||||
|
' <time>2018-03-13T13:47:30+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.679" lon="6.07399">'
|
||||||
|
' <ele>980</ele>'
|
||||||
|
' <time>2018-03-13T13:47:40+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67896" lon="6.07402">'
|
||||||
|
' <ele>980</ele>'
|
||||||
|
' <time>2018-03-13T13:47:45+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67884" lon="6.07408">'
|
||||||
|
' <ele>979</ele>'
|
||||||
|
' <time>2018-03-13T13:47:55+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67863" lon="6.07423">'
|
||||||
|
' <ele>981</ele>'
|
||||||
|
' <time>2018-03-13T13:48:15+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67858" lon="6.07425">'
|
||||||
|
' <ele>980</ele>'
|
||||||
|
' <time>2018-03-13T13:48:20+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67842" lon="6.07434">'
|
||||||
|
' <ele>979</ele>'
|
||||||
|
' <time>2018-03-13T13:48:35+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67837" lon="6.07435">'
|
||||||
|
' <ele>979</ele>'
|
||||||
|
' <time>2018-03-13T13:48:40+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67822" lon="6.07442">'
|
||||||
|
' <ele>975</ele>'
|
||||||
|
' <time>2018-03-13T13:48:55+01:00</time>'
|
||||||
|
' </trkpt>'
|
||||||
|
' </trkseg>'
|
||||||
|
' </trk>'
|
||||||
|
'</gpx>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def gpx_file_wo_track() -> str:
|
def gpx_file_wo_track() -> str:
|
||||||
return (
|
return (
|
||||||
|
@ -2,7 +2,7 @@ from unittest.mock import call, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from gpxpy.gpx import MovingData
|
from gpxpy.gpx import IGNORE_TOP_SPEED_PERCENTILES, MovingData
|
||||||
from werkzeug.datastructures import FileStorage
|
from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
from fittrackee.users.models import User, UserSportPreference
|
from fittrackee.users.models import User, UserSportPreference
|
||||||
@ -55,7 +55,12 @@ class TestStoppedSpeedThreshold:
|
|||||||
assert gpx_track_segment_mock.call_args_list[0] == call(
|
assert gpx_track_segment_mock.call_args_list[0] == call(
|
||||||
stopped_speed_threshold=expected_threshold
|
stopped_speed_threshold=expected_threshold
|
||||||
)
|
)
|
||||||
gpx_track_segment_mock.assert_called_with(expected_threshold)
|
gpx_track_segment_mock.assert_called_with(
|
||||||
|
expected_threshold, # stopped_speed_threshold
|
||||||
|
False, # raw
|
||||||
|
IGNORE_TOP_SPEED_PERCENTILES, # speed_extreemes_percentiles
|
||||||
|
True, # ignore_nonstandard_distances
|
||||||
|
)
|
||||||
|
|
||||||
def test_it_calls_get_moving_data_with_threshold_depending_from_user_preference( # noqa
|
def test_it_calls_get_moving_data_with_threshold_depending_from_user_preference( # noqa
|
||||||
self,
|
self,
|
||||||
@ -85,4 +90,9 @@ class TestStoppedSpeedThreshold:
|
|||||||
assert gpx_track_segment_mock.call_args_list[0] == call(
|
assert gpx_track_segment_mock.call_args_list[0] == call(
|
||||||
stopped_speed_threshold=expected_threshold
|
stopped_speed_threshold=expected_threshold
|
||||||
)
|
)
|
||||||
gpx_track_segment_mock.assert_called_with(expected_threshold)
|
gpx_track_segment_mock.assert_called_with(
|
||||||
|
expected_threshold, # stopped_speed_threshold
|
||||||
|
False, # raw
|
||||||
|
IGNORE_TOP_SPEED_PERCENTILES, # speed_extreemes_percentiles
|
||||||
|
True, # ignore_nonstandard_distances
|
||||||
|
)
|
||||||
|
@ -1,9 +1,27 @@
|
|||||||
|
from datetime import datetime
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
from typing import List
|
from typing import List, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytz
|
||||||
|
from gpxpy.gpxfield import SimpleTZ
|
||||||
|
|
||||||
from fittrackee.workouts.utils.workouts import get_average_speed
|
from fittrackee.workouts.utils.workouts import (
|
||||||
|
get_average_speed,
|
||||||
|
get_workout_datetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
utc_datetime = datetime(
|
||||||
|
year=2022, month=6, day=11, hour=10, minute=23, second=00, tzinfo=pytz.utc
|
||||||
|
)
|
||||||
|
input_workout_dates = [
|
||||||
|
utc_datetime,
|
||||||
|
utc_datetime.replace(tzinfo=None),
|
||||||
|
utc_datetime.replace(tzinfo=SimpleTZ('Z')),
|
||||||
|
utc_datetime.astimezone(pytz.timezone('Europe/Paris')),
|
||||||
|
utc_datetime.astimezone(pytz.timezone('America/Toronto')),
|
||||||
|
'2022-06-11 12:23:00',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestWorkoutAverageSpeed:
|
class TestWorkoutAverageSpeed:
|
||||||
@ -30,3 +48,68 @@ class TestWorkoutAverageSpeed:
|
|||||||
assert get_average_speed(
|
assert get_average_speed(
|
||||||
nb_workouts, total_average_speed, workout_average_speed
|
nb_workouts, total_average_speed, workout_average_speed
|
||||||
) == mean(ave_speeds_list)
|
) == mean(ave_speeds_list)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorkoutGetWorkoutDatetime:
|
||||||
|
@pytest.mark.parametrize('input_workout_date', input_workout_dates)
|
||||||
|
def test_it_returns_naive_datetime(
|
||||||
|
self, input_workout_date: Union[datetime, str]
|
||||||
|
) -> None:
|
||||||
|
naive_workout_date, _ = get_workout_datetime(
|
||||||
|
workout_date=input_workout_date, user_timezone='Europe/Paris'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert naive_workout_date == datetime(
|
||||||
|
year=2022, month=6, day=11, hour=10, minute=23, second=00
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_it_return_naive_datetime_when_no_user_timezone(self) -> None:
|
||||||
|
naive_workout_date, _ = get_workout_datetime(
|
||||||
|
workout_date='2022-06-11 12:23:00', user_timezone=None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert naive_workout_date == datetime(
|
||||||
|
year=2022, month=6, day=11, hour=12, minute=23, second=00
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('input_workout_date', input_workout_dates)
|
||||||
|
def test_it_returns_datetime_with_user_timezone(
|
||||||
|
self, input_workout_date: Union[datetime, str]
|
||||||
|
) -> None:
|
||||||
|
timezone = 'Europe/Paris'
|
||||||
|
|
||||||
|
_, workout_date_with_tz = get_workout_datetime(
|
||||||
|
input_workout_date, user_timezone=timezone, with_timezone=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert workout_date_with_tz == datetime(
|
||||||
|
year=2022,
|
||||||
|
month=6,
|
||||||
|
day=11,
|
||||||
|
hour=10,
|
||||||
|
minute=23,
|
||||||
|
second=00,
|
||||||
|
tzinfo=pytz.utc,
|
||||||
|
).astimezone(pytz.timezone(timezone))
|
||||||
|
|
||||||
|
def test_it_does_not_return_datetime_with_user_timezone_when_no_user_tz(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
_, workout_date_with_tz = get_workout_datetime(
|
||||||
|
workout_date='2022-06-11 12:23:00',
|
||||||
|
user_timezone=None,
|
||||||
|
with_timezone=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert workout_date_with_tz is None
|
||||||
|
|
||||||
|
def test_it_does_not_return_datetime_with_user_timezone_when_with_timezone_to_false( # noqa
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
_, workout_date_with_tz = get_workout_datetime(
|
||||||
|
workout_date='2022-06-11 12:23:00',
|
||||||
|
user_timezone='Europe/Paris',
|
||||||
|
with_timezone=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert workout_date_with_tz is None
|
||||||
|
@ -2,7 +2,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Dict
|
from typing import Dict, Optional
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -92,9 +92,7 @@ def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
|||||||
assert data['data']['workouts'][0]['descent'] == 23.4
|
assert data['data']['workouts'][0]['descent'] == 23.4
|
||||||
assert data['data']['workouts'][0]['distance'] == 0.3
|
assert data['data']['workouts'][0]['distance'] == 0.3
|
||||||
assert data['data']['workouts'][0]['max_alt'] == 998.0
|
assert data['data']['workouts'][0]['max_alt'] == 998.0
|
||||||
assert (
|
assert data['data']['workouts'][0]['max_speed'] == 5.25
|
||||||
data['data']['workouts'][0]['max_speed'] is None
|
|
||||||
) # not enough points
|
|
||||||
assert data['data']['workouts'][0]['min_alt'] == 975.0
|
assert data['data']['workouts'][0]['min_alt'] == 975.0
|
||||||
assert data['data']['workouts'][0]['moving'] == '0:03:55'
|
assert data['data']['workouts'][0]['moving'] == '0:03:55'
|
||||||
assert data['data']['workouts'][0]['pauses'] == '0:00:15'
|
assert data['data']['workouts'][0]['pauses'] == '0:00:15'
|
||||||
@ -114,7 +112,7 @@ def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
|||||||
assert segment['descent'] == 11.0
|
assert segment['descent'] == 11.0
|
||||||
assert segment['distance'] == 0.113
|
assert segment['distance'] == 0.113
|
||||||
assert segment['max_alt'] == 998.0
|
assert segment['max_alt'] == 998.0
|
||||||
assert segment['max_speed'] is None
|
assert segment['max_speed'] == 5.25
|
||||||
assert segment['min_alt'] == 987.0
|
assert segment['min_alt'] == 987.0
|
||||||
assert segment['moving'] == '0:01:30'
|
assert segment['moving'] == '0:01:30'
|
||||||
assert segment['pauses'] is None
|
assert segment['pauses'] is None
|
||||||
@ -128,28 +126,33 @@ def assert_workout_data_with_gpx_segments(data: Dict) -> None:
|
|||||||
assert segment['descent'] == 12.4
|
assert segment['descent'] == 12.4
|
||||||
assert segment['distance'] == 0.186
|
assert segment['distance'] == 0.186
|
||||||
assert segment['max_alt'] == 987.0
|
assert segment['max_alt'] == 987.0
|
||||||
assert segment['max_speed'] is None
|
assert segment['max_speed'] == 5.12
|
||||||
assert segment['min_alt'] == 975.0
|
assert segment['min_alt'] == 975.0
|
||||||
assert segment['moving'] == '0:02:25'
|
assert segment['moving'] == '0:02:25'
|
||||||
assert segment['pauses'] is None
|
assert segment['pauses'] is None
|
||||||
|
|
||||||
records = data['data']['workouts'][0]['records']
|
records = data['data']['workouts'][0]['records']
|
||||||
assert len(records) == 3
|
assert len(records) == 4
|
||||||
assert records[0]['sport_id'] == 1
|
assert records[0]['sport_id'] == 1
|
||||||
assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
|
assert records[0]['workout_id'] == data['data']['workouts'][0]['id']
|
||||||
assert records[0]['record_type'] == 'LD'
|
assert records[0]['record_type'] == 'MS'
|
||||||
assert records[0]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
assert records[0]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||||
assert records[0]['value'] == '0:03:55'
|
assert records[0]['value'] == 5.25
|
||||||
assert records[1]['sport_id'] == 1
|
assert records[1]['sport_id'] == 1
|
||||||
assert records[1]['workout_id'] == data['data']['workouts'][0]['id']
|
assert records[1]['workout_id'] == data['data']['workouts'][0]['id']
|
||||||
assert records[1]['record_type'] == 'FD'
|
assert records[1]['record_type'] == 'LD'
|
||||||
assert records[1]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
assert records[1]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||||
assert records[1]['value'] == 0.3
|
assert records[1]['value'] == '0:03:55'
|
||||||
assert records[2]['sport_id'] == 1
|
assert records[2]['sport_id'] == 1
|
||||||
assert records[2]['workout_id'] == data['data']['workouts'][0]['id']
|
assert records[2]['workout_id'] == data['data']['workouts'][0]['id']
|
||||||
assert records[2]['record_type'] == 'AS'
|
assert records[2]['record_type'] == 'FD'
|
||||||
assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
assert records[2]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||||
assert records[2]['value'] == 4.59
|
assert records[2]['value'] == 0.3
|
||||||
|
assert records[3]['sport_id'] == 1
|
||||||
|
assert records[3]['workout_id'] == data['data']['workouts'][0]['id']
|
||||||
|
assert records[3]['record_type'] == 'AS'
|
||||||
|
assert records[3]['workout_date'] == 'Tue, 13 Mar 2018 12:44:45 GMT'
|
||||||
|
assert records[3]['value'] == 4.59
|
||||||
|
|
||||||
|
|
||||||
def assert_workout_data_wo_gpx(data: Dict) -> None:
|
def assert_workout_data_wo_gpx(data: Dict) -> None:
|
||||||
@ -315,6 +318,41 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
|||||||
)
|
)
|
||||||
assert_workout_data_with_gpx(data)
|
assert_workout_data_with_gpx(data)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('input_user_timezone', [None, 'Europe/Paris'])
|
||||||
|
def test_it_adds_a_workout_with_gpx_with_offset(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
sport_1_cycling: Sport,
|
||||||
|
gpx_file_with_offset: str,
|
||||||
|
input_user_timezone: Optional[str],
|
||||||
|
) -> None:
|
||||||
|
user_1.timezone = input_user_timezone
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/workouts',
|
||||||
|
data=dict(
|
||||||
|
file=(
|
||||||
|
BytesIO(str.encode(gpx_file_with_offset)),
|
||||||
|
'example.gpx',
|
||||||
|
),
|
||||||
|
data='{"sport_id": 1}',
|
||||||
|
),
|
||||||
|
headers=dict(
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
Authorization=f'Bearer {auth_token}',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert 'created' in data['status']
|
||||||
|
assert len(data['data']['workouts']) == 1
|
||||||
|
assert_workout_data_with_gpx(data)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'input_description,input_notes',
|
'input_description,input_notes',
|
||||||
[
|
[
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from datetime import timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import gpxpy.gpx
|
import gpxpy.gpx
|
||||||
|
|
||||||
@ -16,9 +16,9 @@ def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]:
|
|||||||
|
|
||||||
|
|
||||||
def get_gpx_data(
|
def get_gpx_data(
|
||||||
parsed_gpx: gpxpy.gpx,
|
parsed_gpx: Union[gpxpy.gpx.GPX, gpxpy.gpx.GPXTrackSegment],
|
||||||
max_speed: float,
|
max_speed: float,
|
||||||
start: int,
|
start: Union[datetime, None],
|
||||||
stopped_time_between_seg: timedelta,
|
stopped_time_between_seg: timedelta,
|
||||||
stopped_speed_threshold: float,
|
stopped_speed_threshold: float,
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
@ -32,7 +32,8 @@ def get_gpx_data(
|
|||||||
|
|
||||||
duration = parsed_gpx.get_duration()
|
duration = parsed_gpx.get_duration()
|
||||||
gpx_data['duration'] = (
|
gpx_data['duration'] = (
|
||||||
timedelta(seconds=duration) + stopped_time_between_seg
|
timedelta(seconds=duration if duration else 0)
|
||||||
|
+ stopped_time_between_seg
|
||||||
)
|
)
|
||||||
|
|
||||||
ele = parsed_gpx.get_elevation_extremes()
|
ele = parsed_gpx.get_elevation_extremes()
|
||||||
@ -43,18 +44,24 @@ def get_gpx_data(
|
|||||||
gpx_data['uphill'] = hill.uphill
|
gpx_data['uphill'] = hill.uphill
|
||||||
gpx_data['downhill'] = hill.downhill
|
gpx_data['downhill'] = hill.downhill
|
||||||
|
|
||||||
mv = parsed_gpx.get_moving_data(
|
moving_data = parsed_gpx.get_moving_data(
|
||||||
stopped_speed_threshold=stopped_speed_threshold
|
stopped_speed_threshold=stopped_speed_threshold
|
||||||
)
|
)
|
||||||
gpx_data['moving_time'] = timedelta(seconds=mv.moving_time)
|
if moving_data:
|
||||||
gpx_data['stop_time'] = (
|
gpx_data['moving_time'] = timedelta(seconds=moving_data.moving_time)
|
||||||
timedelta(seconds=mv.stopped_time) + stopped_time_between_seg
|
gpx_data['stop_time'] = (
|
||||||
)
|
timedelta(seconds=moving_data.stopped_time)
|
||||||
distance = mv.moving_distance + mv.stopped_distance
|
+ stopped_time_between_seg
|
||||||
gpx_data['distance'] = distance / 1000
|
)
|
||||||
|
distance = moving_data.moving_distance + moving_data.stopped_distance
|
||||||
|
gpx_data['distance'] = distance / 1000
|
||||||
|
|
||||||
average_speed = distance / mv.moving_time if mv.moving_time > 0 else 0
|
average_speed = (
|
||||||
gpx_data['average_speed'] = (average_speed / 1000) * 3600
|
distance / moving_data.moving_time
|
||||||
|
if moving_data.moving_time > 0
|
||||||
|
else 0
|
||||||
|
)
|
||||||
|
gpx_data['average_speed'] = (average_speed / 1000) * 3600
|
||||||
|
|
||||||
return gpx_data
|
return gpx_data
|
||||||
|
|
||||||
@ -72,9 +79,9 @@ def get_gpx_info(
|
|||||||
if gpx is None:
|
if gpx is None:
|
||||||
raise WorkoutGPXException('not found', 'No gpx file')
|
raise WorkoutGPXException('not found', 'No gpx file')
|
||||||
|
|
||||||
gpx_data = {'name': gpx.tracks[0].name, 'segments': []}
|
gpx_data: Dict = {'name': gpx.tracks[0].name, 'segments': []}
|
||||||
max_speed = 0
|
max_speed = 0.0
|
||||||
start = 0
|
start: Optional[datetime] = None
|
||||||
map_data = []
|
map_data = []
|
||||||
weather_data = []
|
weather_data = []
|
||||||
segments_nb = len(gpx.tracks[0].segments)
|
segments_nb = len(gpx.tracks[0].segments)
|
||||||
@ -83,14 +90,15 @@ def get_gpx_info(
|
|||||||
stopped_time_between_seg = no_stopped_time
|
stopped_time_between_seg = no_stopped_time
|
||||||
|
|
||||||
for segment_idx, segment in enumerate(gpx.tracks[0].segments):
|
for segment_idx, segment in enumerate(gpx.tracks[0].segments):
|
||||||
segment_start = 0
|
segment_start: Optional[datetime] = None
|
||||||
segment_points_nb = len(segment.points)
|
segment_points_nb = len(segment.points)
|
||||||
for point_idx, point in enumerate(segment.points):
|
for point_idx, point in enumerate(segment.points):
|
||||||
if point_idx == 0:
|
if point_idx == 0:
|
||||||
|
segment_start = point.time
|
||||||
# first gpx point => get weather
|
# first gpx point => get weather
|
||||||
if start == 0:
|
if start is None:
|
||||||
start = point.time
|
start = point.time
|
||||||
if update_weather_data:
|
if point.time and update_weather_data:
|
||||||
weather_data.append(get_weather(point))
|
weather_data.append(get_weather(point))
|
||||||
|
|
||||||
# if a previous segment exists, calculate stopped time between
|
# if a previous segment exists, calculate stopped time between
|
||||||
@ -108,13 +116,19 @@ def get_gpx_info(
|
|||||||
|
|
||||||
if update_map_data:
|
if update_map_data:
|
||||||
map_data.append([point.longitude, point.latitude])
|
map_data.append([point.longitude, point.latitude])
|
||||||
calculated_max_speed = segment.get_moving_data(
|
moving_data = segment.get_moving_data(
|
||||||
stopped_speed_threshold=stopped_speed_threshold
|
stopped_speed_threshold=stopped_speed_threshold
|
||||||
).max_speed
|
)
|
||||||
segment_max_speed = calculated_max_speed if calculated_max_speed else 0
|
if moving_data:
|
||||||
|
calculated_max_speed = moving_data.max_speed
|
||||||
|
segment_max_speed = (
|
||||||
|
calculated_max_speed if calculated_max_speed else 0
|
||||||
|
)
|
||||||
|
|
||||||
if segment_max_speed > max_speed:
|
if segment_max_speed > max_speed:
|
||||||
max_speed = segment_max_speed
|
max_speed = segment_max_speed
|
||||||
|
else:
|
||||||
|
segment_max_speed = 0.0
|
||||||
|
|
||||||
segment_data = get_gpx_data(
|
segment_data = get_gpx_data(
|
||||||
segment,
|
segment,
|
||||||
@ -137,12 +151,16 @@ def get_gpx_info(
|
|||||||
|
|
||||||
if update_map_data:
|
if update_map_data:
|
||||||
bounds = gpx.get_bounds()
|
bounds = gpx.get_bounds()
|
||||||
gpx_data['bounds'] = [
|
gpx_data['bounds'] = (
|
||||||
bounds.min_latitude,
|
[
|
||||||
bounds.min_longitude,
|
bounds.min_latitude,
|
||||||
bounds.max_latitude,
|
bounds.min_longitude,
|
||||||
bounds.max_longitude,
|
bounds.max_latitude,
|
||||||
]
|
bounds.max_longitude,
|
||||||
|
]
|
||||||
|
if bounds
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
|
||||||
return gpx_data, map_data, weather_data
|
return gpx_data, map_data, weather_data
|
||||||
|
|
||||||
@ -222,7 +240,11 @@ def get_chart_data(
|
|||||||
'latitude': point.latitude,
|
'latitude': point.latitude,
|
||||||
'longitude': point.longitude,
|
'longitude': point.longitude,
|
||||||
'speed': speed,
|
'speed': speed,
|
||||||
'time': point.time,
|
# workaround
|
||||||
|
# https://github.com/tkrajina/gpxpy/issues/209
|
||||||
|
'time': point.time.replace(
|
||||||
|
tzinfo=timezone(point.time.utcoffset())
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
previous_point = point
|
previous_point = point
|
||||||
|
@ -3,18 +3,22 @@ from typing import Dict, Optional
|
|||||||
|
|
||||||
import forecastio
|
import forecastio
|
||||||
import pytz
|
import pytz
|
||||||
from gpxpy.gpx import GPXRoutePoint
|
from gpxpy.gpx import GPXTrackPoint
|
||||||
|
|
||||||
from fittrackee import appLog
|
from fittrackee import appLog
|
||||||
|
|
||||||
API_KEY = os.getenv('WEATHER_API_KEY')
|
API_KEY = os.getenv('WEATHER_API_KEY')
|
||||||
|
|
||||||
|
|
||||||
def get_weather(point: GPXRoutePoint) -> Optional[Dict]:
|
def get_weather(point: GPXTrackPoint) -> Optional[Dict]:
|
||||||
if not API_KEY or API_KEY == '':
|
if not API_KEY or not point.time:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
point_time = pytz.utc.localize(point.time)
|
point_time = (
|
||||||
|
pytz.utc.localize(point.time)
|
||||||
|
if point.time.tzinfo is None
|
||||||
|
else point.time.astimezone(pytz.utc)
|
||||||
|
)
|
||||||
forecast = forecastio.load_forecast(
|
forecast = forecastio.load_forecast(
|
||||||
API_KEY,
|
API_KEY,
|
||||||
point.latitude,
|
point.latitude,
|
||||||
|
@ -22,31 +22,42 @@ from .gpx import get_gpx_info
|
|||||||
from .maps import generate_map, get_map_hash
|
from .maps import generate_map, get_map_hash
|
||||||
|
|
||||||
|
|
||||||
def get_datetime_with_tz(
|
def get_workout_datetime(
|
||||||
timezone: str, workout_date: datetime, gpx_data: Optional[Dict] = None
|
workout_date: Union[datetime, str],
|
||||||
) -> Tuple[Optional[datetime], datetime]:
|
user_timezone: Optional[str],
|
||||||
|
date_str_format: Optional[str] = None,
|
||||||
|
with_timezone: bool = False,
|
||||||
|
) -> Tuple[datetime, Optional[datetime]]:
|
||||||
"""
|
"""
|
||||||
Return naive datetime and datetime with user timezone
|
Return naive datetime and datetime with user timezone if with_timezone
|
||||||
"""
|
"""
|
||||||
workout_date_tz = None
|
workout_date_with_user_tz = None
|
||||||
if timezone:
|
|
||||||
user_tz = pytz.timezone(timezone)
|
|
||||||
utc_tz = pytz.utc
|
|
||||||
if gpx_data:
|
|
||||||
# workout date in gpx is in UTC, but in naive datetime
|
|
||||||
fmt = '%Y-%m-%d %H:%M:%S'
|
|
||||||
workout_date_string = workout_date.strftime(fmt)
|
|
||||||
workout_date_tmp = utc_tz.localize(
|
|
||||||
datetime.strptime(workout_date_string, fmt)
|
|
||||||
)
|
|
||||||
workout_date_tz = workout_date_tmp.astimezone(user_tz)
|
|
||||||
else:
|
|
||||||
workout_date_tz = user_tz.localize(workout_date)
|
|
||||||
workout_date = workout_date_tz.astimezone(utc_tz)
|
|
||||||
# make datetime 'naive' like in gpx file
|
|
||||||
workout_date = workout_date.replace(tzinfo=None)
|
|
||||||
|
|
||||||
return workout_date_tz, workout_date
|
# workout w/o gpx
|
||||||
|
if isinstance(workout_date, str):
|
||||||
|
if not date_str_format:
|
||||||
|
date_str_format = '%Y-%m-%d %H:%M:%S'
|
||||||
|
workout_date = datetime.strptime(workout_date, date_str_format)
|
||||||
|
if user_timezone:
|
||||||
|
workout_date = pytz.timezone(user_timezone).localize(workout_date)
|
||||||
|
|
||||||
|
if workout_date.tzinfo is None:
|
||||||
|
naive_workout_date = workout_date
|
||||||
|
if user_timezone and with_timezone:
|
||||||
|
pytz.utc.localize(naive_workout_date)
|
||||||
|
workout_date_with_user_tz = pytz.utc.localize(
|
||||||
|
naive_workout_date
|
||||||
|
).astimezone(pytz.timezone(user_timezone))
|
||||||
|
else:
|
||||||
|
naive_workout_date = workout_date.astimezone(pytz.utc).replace(
|
||||||
|
tzinfo=None
|
||||||
|
)
|
||||||
|
if user_timezone and with_timezone:
|
||||||
|
workout_date_with_user_tz = workout_date.astimezone(
|
||||||
|
pytz.timezone(user_timezone)
|
||||||
|
)
|
||||||
|
|
||||||
|
return naive_workout_date, workout_date_with_user_tz
|
||||||
|
|
||||||
|
|
||||||
def get_datetime_from_request_args(
|
def get_datetime_from_request_args(
|
||||||
@ -57,25 +68,32 @@ def get_datetime_from_request_args(
|
|||||||
|
|
||||||
date_from_str = params.get('from')
|
date_from_str = params.get('from')
|
||||||
if date_from_str:
|
if date_from_str:
|
||||||
date_from = datetime.strptime(date_from_str, '%Y-%m-%d')
|
date_from, _ = get_workout_datetime(
|
||||||
_, date_from = get_datetime_with_tz(user.timezone, date_from)
|
workout_date=date_from_str,
|
||||||
|
user_timezone=user.timezone,
|
||||||
|
date_str_format='%Y-%m-%d',
|
||||||
|
)
|
||||||
date_to_str = params.get('to')
|
date_to_str = params.get('to')
|
||||||
if date_to_str:
|
if date_to_str:
|
||||||
date_to = datetime.strptime(
|
date_to, _ = get_workout_datetime(
|
||||||
f'{date_to_str} 23:59:59', '%Y-%m-%d %H:%M:%S'
|
workout_date=f'{date_to_str} 23:59:59',
|
||||||
|
user_timezone=user.timezone,
|
||||||
)
|
)
|
||||||
_, date_to = get_datetime_with_tz(user.timezone, date_to)
|
|
||||||
return date_from, date_to
|
return date_from, date_to
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_microseconds(delta: timedelta) -> timedelta:
|
||||||
|
return delta - timedelta(microseconds=delta.microseconds)
|
||||||
|
|
||||||
|
|
||||||
def update_workout_data(
|
def update_workout_data(
|
||||||
workout: Union[Workout, WorkoutSegment], gpx_data: Dict
|
workout: Union[Workout, WorkoutSegment], gpx_data: Dict
|
||||||
) -> Union[Workout, WorkoutSegment]:
|
) -> Union[Workout, WorkoutSegment]:
|
||||||
"""
|
"""
|
||||||
Update workout or workout segment with data from gpx file
|
Update workout or workout segment with data from gpx file
|
||||||
"""
|
"""
|
||||||
workout.pauses = gpx_data['stop_time']
|
workout.pauses = _remove_microseconds(gpx_data['stop_time'])
|
||||||
workout.moving = gpx_data['moving_time']
|
workout.moving = _remove_microseconds(gpx_data['moving_time'])
|
||||||
workout.min_alt = gpx_data['elevation_min']
|
workout.min_alt = gpx_data['elevation_min']
|
||||||
workout.max_alt = gpx_data['elevation_max']
|
workout.max_alt = gpx_data['elevation_max']
|
||||||
workout.descent = gpx_data['downhill']
|
workout.descent = gpx_data['downhill']
|
||||||
@ -92,17 +110,17 @@ def create_workout(
|
|||||||
Create Workout from data entered by user and from gpx if a gpx file is
|
Create Workout from data entered by user and from gpx if a gpx file is
|
||||||
provided
|
provided
|
||||||
"""
|
"""
|
||||||
workout_date = (
|
workout_date, workout_date_tz = get_workout_datetime(
|
||||||
gpx_data['start']
|
workout_date=gpx_data['start']
|
||||||
if gpx_data
|
if gpx_data
|
||||||
else datetime.strptime(workout_data['workout_date'], '%Y-%m-%d %H:%M')
|
else workout_data['workout_date'],
|
||||||
)
|
date_str_format=None if gpx_data else '%Y-%m-%d %H:%M',
|
||||||
workout_date_tz, workout_date = get_datetime_with_tz(
|
user_timezone=user.timezone,
|
||||||
user.timezone, workout_date, gpx_data
|
with_timezone=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
duration = (
|
duration = (
|
||||||
gpx_data['duration']
|
_remove_microseconds(gpx_data['duration'])
|
||||||
if gpx_data
|
if gpx_data
|
||||||
else timedelta(seconds=workout_data['duration'])
|
else timedelta(seconds=workout_data['duration'])
|
||||||
)
|
)
|
||||||
@ -202,11 +220,10 @@ def edit_workout(
|
|||||||
workout.notes = workout_data.get('notes')
|
workout.notes = workout_data.get('notes')
|
||||||
if not workout.gpx:
|
if not workout.gpx:
|
||||||
if workout_data.get('workout_date'):
|
if workout_data.get('workout_date'):
|
||||||
workout_date = datetime.strptime(
|
workout.workout_date, _ = get_workout_datetime(
|
||||||
workout_data['workout_date'], '%Y-%m-%d %H:%M'
|
workout_date=workout_data.get('workout_date', ''),
|
||||||
)
|
date_str_format='%Y-%m-%d %H:%M',
|
||||||
_, workout.workout_date = get_datetime_with_tz(
|
user_timezone=auth_user.timezone,
|
||||||
auth_user.timezone, workout_date
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if workout_data.get('duration'):
|
if workout_data.get('duration'):
|
||||||
|
@ -1111,7 +1111,8 @@ def post_workout_no_gpx(
|
|||||||
"status": "success"
|
"status": "success"
|
||||||
}
|
}
|
||||||
|
|
||||||
:<json string workout_date: workout date (format: ``%Y-%m-%d %H:%M``)
|
:<json string workout_date: workout date, in user timezone
|
||||||
|
(format: ``%Y-%m-%d %H:%M``)
|
||||||
:<json float distance: workout distance in km
|
:<json float distance: workout distance in km
|
||||||
:<json integer duration: workout duration in seconds
|
:<json integer duration: workout duration in seconds
|
||||||
:<json string notes: notes (not mandatory)
|
:<json string notes: notes (not mandatory)
|
||||||
@ -1261,7 +1262,8 @@ def update_workout(
|
|||||||
|
|
||||||
:param string workout_short_id: workout short id
|
:param string workout_short_id: workout short id
|
||||||
|
|
||||||
:<json string workout_date: workout date (format: ``%Y-%m-%d %H:%M``)
|
:<json string workout_date: workout date in user timezone
|
||||||
|
(format: ``%Y-%m-%d %H:%M``)
|
||||||
(only for workout without gpx)
|
(only for workout without gpx)
|
||||||
:<json float distance: workout distance in km
|
:<json float distance: workout distance in km
|
||||||
(only for workout without gpx)
|
(only for workout without gpx)
|
||||||
|
8
poetry.lock
generated
8
poetry.lock
generated
@ -386,11 +386,11 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpxpy"
|
name = "gpxpy"
|
||||||
version = "1.3.4"
|
version = "1.5.0"
|
||||||
description = "GPX file parser and GPS track manipulation library"
|
description = "GPX file parser and GPS track manipulation library"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
@ -1466,7 +1466,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.7"
|
python-versions = "^3.7"
|
||||||
content-hash = "fa5be5cc59de72ae3ddb18acb2c0291a7d77296de6427492b46c40d794800335"
|
content-hash = "40b8491c8b82a7b29f57a2845209965c6351b527aebb7d504f5f8ec0ace4141e"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
alabaster = [
|
alabaster = [
|
||||||
@ -1727,7 +1727,7 @@ gitpython = [
|
|||||||
{file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
|
{file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"},
|
||||||
]
|
]
|
||||||
gpxpy = [
|
gpxpy = [
|
||||||
{file = "gpxpy-1.3.4.tar.gz", hash = "sha256:4a0f072ae5bdf9270c7450e452f93a6c5c91d888114e8d78868a8f163b0dbb15"},
|
{file = "gpxpy-1.5.0.tar.gz", hash = "sha256:e6993a8945eae07a833cd304b88bbc6c3c132d63b2bf4a9b0a5d9097616b8708"},
|
||||||
]
|
]
|
||||||
greenlet = [
|
greenlet = [
|
||||||
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
|
{file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"},
|
||||||
|
@ -29,7 +29,7 @@ flask = "^2.1"
|
|||||||
flask-bcrypt = "^1.0"
|
flask-bcrypt = "^1.0"
|
||||||
flask-dramatiq = "^0.6.0"
|
flask-dramatiq = "^0.6.0"
|
||||||
flask-migrate = "^3.1"
|
flask-migrate = "^3.1"
|
||||||
gpxpy = "=1.3.4"
|
gpxpy = "=1.5.0"
|
||||||
gunicorn = "^20.1"
|
gunicorn = "^20.1"
|
||||||
humanize = "^4.1"
|
humanize = "^4.1"
|
||||||
psycopg2-binary = "^2.9"
|
psycopg2-binary = "^2.9"
|
||||||
|
Loading…
Reference in New Issue
Block a user