mirror of
https://github.com/workhardbekind/workout-challenge.git
synced 2026-07-04 09:23:32 -04:00
first commit
This commit is contained in:
commit
e7f627801f
152 changed files with 35352 additions and 0 deletions
194
src-backend/custom_user/strava.py
Normal file
194
src-backend/custom_user/strava.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import requests
|
||||
import time, datetime
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.db import IntegrityError
|
||||
from workout_challenge.celery import app, is_task_already_executing
|
||||
from django.db.models import Q
|
||||
|
||||
from workouts.models import Workout
|
||||
from .api_rate_limiter import strava_api_monitor, RateLimitExceeded # Import to trigger initialization
|
||||
|
||||
|
||||
def _seconds_until_next_interval():
|
||||
current_time = time.localtime()
|
||||
minutes = current_time.tm_min
|
||||
seconds = current_time.tm_sec
|
||||
|
||||
# Calculate the next interval (15, 30, 45, or 0 of the next hour)
|
||||
next_interval = (minutes // 15 + 1) * 15
|
||||
if next_interval == 60: # Handle the case where it rolls over to the next hour
|
||||
next_interval = 0
|
||||
|
||||
# Calculate the seconds until the next interval
|
||||
seconds_until = (next_interval - minutes) * 60 - seconds
|
||||
if next_interval == 0: # Adjust for the next hour
|
||||
seconds_until += 3600
|
||||
|
||||
return seconds_until
|
||||
|
||||
|
||||
@app.task(bind=True, time_limit=60 * 60 * 3, max_retries=10) # 3 hour time limit
|
||||
def daily_strava_sync(self, refresh_all=False):
|
||||
if is_task_already_executing('daily_strava_sync'):
|
||||
return 'Task already executing. Skipping.'
|
||||
|
||||
CustomUser = get_user_model()
|
||||
user_lst = CustomUser.objects.filter(
|
||||
strava_refresh_token__isnull=False,
|
||||
is_active=True
|
||||
)
|
||||
if refresh_all is False:
|
||||
user_lst = user_lst.filter(
|
||||
Q(strava_last_synced_at__lt=timezone.now() - datetime.timedelta(hours=6)) |
|
||||
Q(strava_last_synced_at__isnull=True)
|
||||
)
|
||||
user_lst = user_lst.order_by('strava_last_synced_at', 'pk')
|
||||
|
||||
user_lst_names = [{'pk': i.pk, 'username': i.username, 'email': i.email} for i in user_lst]
|
||||
print(f'Syncing Strava for {len(user_lst)} users: {user_lst_names}')
|
||||
|
||||
for user in user_lst:
|
||||
try:
|
||||
sync_strava(user__id=user.id)
|
||||
except RateLimitExceeded as exc:
|
||||
sleep_time = _seconds_until_next_interval() + 60
|
||||
print(f'Strava sync rate limit exceeded - sleeping for {sleep_time // 60 } mins')
|
||||
raise self.retry(exc=exc, countdown=sleep_time) # retry in next Strava 15min api period
|
||||
except Exception as exc:
|
||||
print(f'Strava sync failed for user {user.email} - {exc}')
|
||||
|
||||
print('Finished syncing Strava.')
|
||||
return user_lst_names
|
||||
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
def sync_strava(self, user__id, start_datetime=None):
|
||||
access_token = cache.get(f"strava_access_token_{user__id}")
|
||||
CustomUser = get_user_model()
|
||||
user = CustomUser.objects.get(id=user__id)
|
||||
|
||||
all_existing_strava_activities = set(Workout.objects.all().values_list('strava_id', flat=True))
|
||||
|
||||
cnt_new_strava_activities = 0
|
||||
cnt_updated_strava_activities = 0
|
||||
|
||||
if strava_api_monitor.ok_workout_requests() is False:
|
||||
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
|
||||
|
||||
# refresh access token if expired
|
||||
if access_token is None:
|
||||
refresh_token = user.strava_refresh_token
|
||||
client_id = settings.STRAVA_CLIENT_ID
|
||||
client_secret = settings.STRAVA_CLIENT_SECRET
|
||||
|
||||
response = requests.post(
|
||||
url='https://www.strava.com/oauth/token',
|
||||
data={
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refresh_token,
|
||||
}
|
||||
)
|
||||
strava_api_monitor.log_request(response)
|
||||
response.raise_for_status()
|
||||
|
||||
strava_tokens = response.json()
|
||||
access_token = strava_tokens.get('access_token', None)
|
||||
cache.set(f"strava_access_token_{user__id}", access_token, int(strava_tokens.get('expires_in', 21600)) - 60)
|
||||
|
||||
|
||||
# get activities
|
||||
page = 1
|
||||
per_page = 200
|
||||
while True:
|
||||
if strava_api_monitor.ok_workout_requests() is False:
|
||||
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
|
||||
|
||||
response = requests.get(
|
||||
url='https://www.strava.com/api/v3/athlete/activities',
|
||||
headers={
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
},
|
||||
params={
|
||||
'after': None if start_datetime is None else int(start_datetime.timestamp()),
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
}
|
||||
)
|
||||
strava_api_monitor.log_request(response)
|
||||
response.raise_for_status()
|
||||
activities = response.json()
|
||||
|
||||
for activity in activities:
|
||||
activity_id = activity.get('id')
|
||||
|
||||
props = {
|
||||
'user': user,
|
||||
'strava_id': activity_id,
|
||||
'sport_type': activity.get('sport_type'),
|
||||
'start_datetime': datetime.datetime.fromisoformat(activity.get('start_date')),
|
||||
'duration': datetime.timedelta(seconds=activity.get('moving_time')),
|
||||
'distance': None if activity.get('distance') == 0 else activity.get('distance') / 1_000,
|
||||
}
|
||||
|
||||
# if existing workout - update activity details
|
||||
if activity_id in all_existing_strava_activities:
|
||||
workout = Workout.objects.get(strava_id=activity_id)
|
||||
for key, value in props.items():
|
||||
setattr(workout, key, value)
|
||||
workout.save()
|
||||
cnt_updated_strava_activities += 1
|
||||
|
||||
# if a new workout - get activity details
|
||||
else:
|
||||
if strava_api_monitor.ok_workout_requests() is False:
|
||||
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
|
||||
|
||||
response = requests.get(
|
||||
url=f'https://www.strava.com/api/v3/activities/{activity_id}',
|
||||
headers={
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
},
|
||||
)
|
||||
strava_api_monitor.log_request(response)
|
||||
response.raise_for_status()
|
||||
activity_details = response.json()
|
||||
|
||||
avg_heart_rate = activity_details.get('average_heartrate', 0)
|
||||
props['kcal'] = kcal = activity_details.get('calories', activity_details.get('kilojoules', 0) / 4.18)
|
||||
props['strava_intensity_avg_watts'] = avg_watt = activity_details.get('average_watts', 0)
|
||||
|
||||
# estimate intensity
|
||||
max_heart_rate = 180
|
||||
kcal_per_ten_minute = kcal / (max(activity.get('moving_time', 60 * 30), 60) / (60 * 10))
|
||||
if avg_heart_rate > max_heart_rate * 0.85 or kcal_per_ten_minute > 120 or avg_watt > 300:
|
||||
props['intensity_category'] = 4
|
||||
elif avg_heart_rate > max_heart_rate * 0.70 or kcal_per_ten_minute > 90 or avg_watt > 275:
|
||||
props['intensity_category'] = 3
|
||||
elif avg_heart_rate > max_heart_rate * 0.60 or kcal_per_ten_minute > 75 or avg_watt > 225:
|
||||
props['intensity_category'] = 2
|
||||
else:
|
||||
props['intensity_category'] = 1
|
||||
|
||||
Workout.objects.create(**props)
|
||||
cnt_new_strava_activities += 1
|
||||
|
||||
if len(activities) < per_page:
|
||||
break
|
||||
page += 1
|
||||
|
||||
strava_last_synced_at = timezone.now()
|
||||
if start_datetime is None:
|
||||
setattr(user, 'strava_last_synced_at', strava_last_synced_at)
|
||||
user.save()
|
||||
print(f'User {user__id} - fetched {cnt_new_strava_activities} new strava activities and updated {cnt_updated_strava_activities} existing strava activities')
|
||||
|
||||
return {'user': user__id, 'total_activities': (page - 1) * per_page + len(activities), 'new_activities': cnt_new_strava_activities, 'updated_activities': cnt_updated_strava_activities, 'sync_time': strava_last_synced_at}
|
||||
Loading…
Add table
Add a link
Reference in a new issue