diff --git a/fittrackee/tests/fixtures/fixtures_workouts.py b/fittrackee/tests/fixtures/fixtures_workouts.py index 46402c61..e2e11b21 100644 --- a/fittrackee/tests/fixtures/fixtures_workouts.py +++ b/fittrackee/tests/fixtures/fixtures_workouts.py @@ -452,6 +452,120 @@ def gpx_file_wo_name() -> str: ) +@pytest.fixture() +def gpx_file_with_offset() -> str: + return ( + '' + '' # noqa + ' ' + ' ' + ' ' + ' ' + ' 998' + ' ' + ' ' + ' ' + ' 998' + ' ' + ' ' + ' ' + ' 994' + ' ' + ' ' + ' ' + ' 994' + ' ' + ' ' + ' ' + ' 994' + ' ' + ' ' + ' ' + ' 993' + ' ' + ' ' + ' ' + ' 992' + ' ' + ' ' + ' ' + ' 992' + ' ' + ' ' + ' ' + ' 987' + ' ' + ' ' + ' ' + ' 987' + ' ' + ' ' + ' ' + ' 987' + ' ' + ' ' + ' ' + ' 987' + ' ' + ' ' + ' ' + ' 986' + ' ' + ' ' + ' ' + ' 986' + ' ' + ' ' + ' ' + ' 986' + ' ' + ' ' + ' ' + ' 985' + ' ' + ' ' + ' ' + ' 980' + ' ' + ' ' + ' ' + ' 980' + ' ' + ' ' + ' ' + ' 980' + ' ' + ' ' + ' ' + ' 979' + ' ' + ' ' + ' ' + ' 981' + ' ' + ' ' + ' ' + ' 980' + ' ' + ' ' + ' ' + ' 979' + ' ' + ' ' + ' ' + ' 979' + ' ' + ' ' + ' ' + ' 975' + ' ' + ' ' + ' ' + ' ' + '' + ) + + @pytest.fixture() def gpx_file_wo_track() -> str: return ( diff --git a/fittrackee/tests/workouts/test_utils/test_gpx.py b/fittrackee/tests/workouts/test_utils/test_gpx.py index 06d655a0..167b8bba 100644 --- a/fittrackee/tests/workouts/test_utils/test_gpx.py +++ b/fittrackee/tests/workouts/test_utils/test_gpx.py @@ -2,7 +2,7 @@ from unittest.mock import call, patch import pytest from flask import Flask -from gpxpy.gpx import MovingData +from gpxpy.gpx import IGNORE_TOP_SPEED_PERCENTILES, MovingData from werkzeug.datastructures import FileStorage from fittrackee.users.models import User, UserSportPreference @@ -55,7 +55,12 @@ class TestStoppedSpeedThreshold: assert gpx_track_segment_mock.call_args_list[0] == call( 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 self, @@ -85,4 +90,9 @@ class TestStoppedSpeedThreshold: assert gpx_track_segment_mock.call_args_list[0] == call( 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 + ) diff --git a/fittrackee/tests/workouts/test_utils/test_workouts.py b/fittrackee/tests/workouts/test_utils/test_workouts.py index 0e325878..16a90145 100644 --- a/fittrackee/tests/workouts/test_utils/test_workouts.py +++ b/fittrackee/tests/workouts/test_utils/test_workouts.py @@ -1,9 +1,27 @@ +from datetime import datetime from statistics import mean -from typing import List +from typing import List, Union 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: @@ -30,3 +48,68 @@ class TestWorkoutAverageSpeed: assert get_average_speed( nb_workouts, total_average_speed, workout_average_speed ) == 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 diff --git a/fittrackee/tests/workouts/test_workouts_api_1_post.py b/fittrackee/tests/workouts/test_workouts_api_1_post.py index 5e56f003..f75320ec 100644 --- a/fittrackee/tests/workouts/test_workouts_api_1_post.py +++ b/fittrackee/tests/workouts/test_workouts_api_1_post.py @@ -2,7 +2,7 @@ import json import os from datetime import datetime from io import BytesIO -from typing import Dict +from typing import Dict, Optional from unittest.mock import Mock 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]['distance'] == 0.3 assert data['data']['workouts'][0]['max_alt'] == 998.0 - assert ( - data['data']['workouts'][0]['max_speed'] is None - ) # not enough points + assert data['data']['workouts'][0]['max_speed'] == 5.25 assert data['data']['workouts'][0]['min_alt'] == 975.0 assert data['data']['workouts'][0]['moving'] == '0:03:55' 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['distance'] == 0.113 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['moving'] == '0:01:30' 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['distance'] == 0.186 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['moving'] == '0:02:25' assert segment['pauses'] is None records = data['data']['workouts'][0]['records'] - assert len(records) == 3 + assert len(records) == 4 assert records[0]['sport_id'] == 1 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]['value'] == '0:03:55' + assert records[0]['value'] == 5.25 assert records[1]['sport_id'] == 1 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]['value'] == 0.3 + assert records[1]['value'] == '0:03:55' assert records[2]['sport_id'] == 1 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]['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: @@ -315,6 +318,41 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin): ) 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( 'input_description,input_notes', [ diff --git a/fittrackee/workouts/utils/gpx.py b/fittrackee/workouts/utils/gpx.py index f549014f..367c1c3c 100644 --- a/fittrackee/workouts/utils/gpx.py +++ b/fittrackee/workouts/utils/gpx.py @@ -1,5 +1,5 @@ -from datetime import timedelta -from typing import Any, Dict, List, Optional, Tuple +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, List, Optional, Tuple, Union import gpxpy.gpx @@ -16,9 +16,9 @@ def open_gpx_file(gpx_file: str) -> Optional[gpxpy.gpx.GPX]: def get_gpx_data( - parsed_gpx: gpxpy.gpx, + parsed_gpx: Union[gpxpy.gpx.GPX, gpxpy.gpx.GPXTrackSegment], max_speed: float, - start: int, + start: Union[datetime, None], stopped_time_between_seg: timedelta, stopped_speed_threshold: float, ) -> Dict: @@ -32,7 +32,8 @@ def get_gpx_data( duration = parsed_gpx.get_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() @@ -43,18 +44,24 @@ def get_gpx_data( gpx_data['uphill'] = hill.uphill gpx_data['downhill'] = hill.downhill - mv = parsed_gpx.get_moving_data( + moving_data = parsed_gpx.get_moving_data( stopped_speed_threshold=stopped_speed_threshold ) - gpx_data['moving_time'] = timedelta(seconds=mv.moving_time) - gpx_data['stop_time'] = ( - timedelta(seconds=mv.stopped_time) + stopped_time_between_seg - ) - distance = mv.moving_distance + mv.stopped_distance - gpx_data['distance'] = distance / 1000 + if moving_data: + gpx_data['moving_time'] = timedelta(seconds=moving_data.moving_time) + gpx_data['stop_time'] = ( + timedelta(seconds=moving_data.stopped_time) + + stopped_time_between_seg + ) + 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 - gpx_data['average_speed'] = (average_speed / 1000) * 3600 + average_speed = ( + distance / moving_data.moving_time + if moving_data.moving_time > 0 + else 0 + ) + gpx_data['average_speed'] = (average_speed / 1000) * 3600 return gpx_data @@ -72,9 +79,9 @@ def get_gpx_info( if gpx is None: raise WorkoutGPXException('not found', 'No gpx file') - gpx_data = {'name': gpx.tracks[0].name, 'segments': []} - max_speed = 0 - start = 0 + gpx_data: Dict = {'name': gpx.tracks[0].name, 'segments': []} + max_speed = 0.0 + start: Optional[datetime] = None map_data = [] weather_data = [] segments_nb = len(gpx.tracks[0].segments) @@ -83,14 +90,15 @@ def get_gpx_info( stopped_time_between_seg = no_stopped_time for segment_idx, segment in enumerate(gpx.tracks[0].segments): - segment_start = 0 + segment_start: Optional[datetime] = None segment_points_nb = len(segment.points) for point_idx, point in enumerate(segment.points): if point_idx == 0: + segment_start = point.time # first gpx point => get weather - if start == 0: + if start is None: start = point.time - if update_weather_data: + if point.time and update_weather_data: weather_data.append(get_weather(point)) # if a previous segment exists, calculate stopped time between @@ -108,13 +116,19 @@ def get_gpx_info( if update_map_data: 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 - ).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: - max_speed = segment_max_speed + if segment_max_speed > max_speed: + max_speed = segment_max_speed + else: + segment_max_speed = 0.0 segment_data = get_gpx_data( segment, @@ -137,12 +151,16 @@ def get_gpx_info( if update_map_data: bounds = gpx.get_bounds() - gpx_data['bounds'] = [ - bounds.min_latitude, - bounds.min_longitude, - bounds.max_latitude, - bounds.max_longitude, - ] + gpx_data['bounds'] = ( + [ + bounds.min_latitude, + bounds.min_longitude, + bounds.max_latitude, + bounds.max_longitude, + ] + if bounds + else [] + ) return gpx_data, map_data, weather_data @@ -222,7 +240,11 @@ def get_chart_data( 'latitude': point.latitude, 'longitude': point.longitude, '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 diff --git a/fittrackee/workouts/utils/weather.py b/fittrackee/workouts/utils/weather.py index 8d580ad7..79a22932 100644 --- a/fittrackee/workouts/utils/weather.py +++ b/fittrackee/workouts/utils/weather.py @@ -3,18 +3,22 @@ from typing import Dict, Optional import forecastio import pytz -from gpxpy.gpx import GPXRoutePoint +from gpxpy.gpx import GPXTrackPoint from fittrackee import appLog API_KEY = os.getenv('WEATHER_API_KEY') -def get_weather(point: GPXRoutePoint) -> Optional[Dict]: - if not API_KEY or API_KEY == '': +def get_weather(point: GPXTrackPoint) -> Optional[Dict]: + if not API_KEY or not point.time: return None 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( API_KEY, point.latitude, diff --git a/fittrackee/workouts/utils/workouts.py b/fittrackee/workouts/utils/workouts.py index e9159423..dea4c946 100644 --- a/fittrackee/workouts/utils/workouts.py +++ b/fittrackee/workouts/utils/workouts.py @@ -22,31 +22,42 @@ from .gpx import get_gpx_info from .maps import generate_map, get_map_hash -def get_datetime_with_tz( - timezone: str, workout_date: datetime, gpx_data: Optional[Dict] = None -) -> Tuple[Optional[datetime], datetime]: +def get_workout_datetime( + workout_date: Union[datetime, str], + 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 - 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) + workout_date_with_user_tz = 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( @@ -57,25 +68,32 @@ def get_datetime_from_request_args( date_from_str = params.get('from') if date_from_str: - date_from = datetime.strptime(date_from_str, '%Y-%m-%d') - _, date_from = get_datetime_with_tz(user.timezone, date_from) + date_from, _ = get_workout_datetime( + workout_date=date_from_str, + user_timezone=user.timezone, + date_str_format='%Y-%m-%d', + ) date_to_str = params.get('to') if date_to_str: - date_to = datetime.strptime( - f'{date_to_str} 23:59:59', '%Y-%m-%d %H:%M:%S' + date_to, _ = get_workout_datetime( + 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 +def _remove_microseconds(delta: timedelta) -> timedelta: + return delta - timedelta(microseconds=delta.microseconds) + + def update_workout_data( workout: Union[Workout, WorkoutSegment], gpx_data: Dict ) -> Union[Workout, WorkoutSegment]: """ Update workout or workout segment with data from gpx file """ - workout.pauses = gpx_data['stop_time'] - workout.moving = gpx_data['moving_time'] + workout.pauses = _remove_microseconds(gpx_data['stop_time']) + workout.moving = _remove_microseconds(gpx_data['moving_time']) workout.min_alt = gpx_data['elevation_min'] workout.max_alt = gpx_data['elevation_max'] 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 provided """ - workout_date = ( - gpx_data['start'] + workout_date, workout_date_tz = get_workout_datetime( + workout_date=gpx_data['start'] if gpx_data - else datetime.strptime(workout_data['workout_date'], '%Y-%m-%d %H:%M') - ) - workout_date_tz, workout_date = get_datetime_with_tz( - user.timezone, workout_date, gpx_data + else workout_data['workout_date'], + date_str_format=None if gpx_data else '%Y-%m-%d %H:%M', + user_timezone=user.timezone, + with_timezone=True, ) duration = ( - gpx_data['duration'] + _remove_microseconds(gpx_data['duration']) if gpx_data else timedelta(seconds=workout_data['duration']) ) @@ -202,11 +220,10 @@ def edit_workout( workout.notes = workout_data.get('notes') if not workout.gpx: if workout_data.get('workout_date'): - workout_date = datetime.strptime( - workout_data['workout_date'], '%Y-%m-%d %H:%M' - ) - _, workout.workout_date = get_datetime_with_tz( - auth_user.timezone, workout_date + workout.workout_date, _ = get_workout_datetime( + workout_date=workout_data.get('workout_date', ''), + date_str_format='%Y-%m-%d %H:%M', + user_timezone=auth_user.timezone, ) if workout_data.get('duration'): diff --git a/fittrackee/workouts/workouts.py b/fittrackee/workouts/workouts.py index b5d90a32..a6152189 100644 --- a/fittrackee/workouts/workouts.py +++ b/fittrackee/workouts/workouts.py @@ -1111,7 +1111,8 @@ def post_workout_no_gpx( "status": "success" } - :=3.7.4.3", markers = "python_version < \"3.8\"" [[package]] name = "gpxpy" -version = "1.3.4" +version = "1.5.0" description = "GPX file parser and GPS track manipulation library" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [[package]] name = "greenlet" @@ -1466,7 +1466,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "fa5be5cc59de72ae3ddb18acb2c0291a7d77296de6427492b46c40d794800335" +content-hash = "40b8491c8b82a7b29f57a2845209965c6351b527aebb7d504f5f8ec0ace4141e" [metadata.files] alabaster = [ @@ -1727,7 +1727,7 @@ gitpython = [ {file = "GitPython-3.1.27.tar.gz", hash = "sha256:1c885ce809e8ba2d88a29befeb385fcea06338d3640712b59ca623c220bb5704"}, ] gpxpy = [ - {file = "gpxpy-1.3.4.tar.gz", hash = "sha256:4a0f072ae5bdf9270c7450e452f93a6c5c91d888114e8d78868a8f163b0dbb15"}, + {file = "gpxpy-1.5.0.tar.gz", hash = "sha256:e6993a8945eae07a833cd304b88bbc6c3c132d63b2bf4a9b0a5d9097616b8708"}, ] greenlet = [ {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, diff --git a/pyproject.toml b/pyproject.toml index 1dcfdfff..6d13c3b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ flask = "^2.1" flask-bcrypt = "^1.0" flask-dramatiq = "^0.6.0" flask-migrate = "^3.1" -gpxpy = "=1.3.4" +gpxpy = "=1.5.0" gunicorn = "^20.1" humanize = "^4.1" psycopg2-binary = "^2.9"