Criar API REST Django REST Framework para consumir API do Strava, listando atividades do usuário.
pip3 install django djangorestframework python-dotenv requests django-filter djangorestframework-simplejwt --break-system-packages
mkdir strava_drf
cd strava_drf
django-admin startproject config .
mkdir apps
cd apps
django-admin startapp activities
cd ..
# Django
DEBUG=True
SECRET_KEY=cole_sua_secret_key_aqui
ALLOWED_HOSTS=localhost,127.0.0.1
# Database (SQLite para dev)
DATABASE_URL=sqlite:///db.sqlite3
# Strava API
STRAVA_CLIENT_ID=seu_client_id_aqui
STRAVA_CLIENT_SECRET=seu_client_secret_aqui
STRAVA_ACCESS_TOKEN=seu_access_token_aqui
STRAVA_REFRESH_TOKEN=seu_refresh_token_aqui
# JWT / Auth
JWT_ACCESS_TOKEN_LIFETIME_MINUTES=30
JWT_REFRESH_TOKEN_LIFETIME_DAYS=1
# Locale / Timezone
TIME_ZONE=America/Sao_Paulo
from pathlib import Path
from datetime import timedelta
import os
from dotenv import load_dotenv
# Base
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / ".env")
# Security
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = os.getenv("DEBUG", "False") == "True"
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
# Applications
INSTALLED_APPS = [
# Django
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Third-party
"rest_framework",
"django_filters",
"rest_framework_simplejwt",
# Local
"apps.activities",
]
# Middleware
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# URLs / WSGI
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
# Templates
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
# Database (SQLite para desenvolvimento)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
LANGUAGE_CODE = "pt-br"
TIME_ZONE = os.getenv("TIME_ZONE", "UTC")
USE_I18N = True
USE_TZ = True
# Static files
STATIC_URL = "/static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Django REST Framework
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
),
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
],
}
# Simple JWT
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(
minutes=int(os.getenv("JWT_ACCESS_TOKEN_LIFETIME_MINUTES", 30))
),
"REFRESH_TOKEN_LIFETIME": timedelta(
days=int(os.getenv("JWT_REFRESH_TOKEN_LIFETIME_DAYS", 1))
),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"AUTH_HEADER_TYPES": ("Bearer",),
}
# Strava API
STRAVA_CLIENT_ID = os.getenv("STRAVA_CLIENT_ID")
STRAVA_CLIENT_SECRET = os.getenv("STRAVA_CLIENT_SECRET")
STRAVA_ACCESS_TOKEN = os.getenv("STRAVA_ACCESS_TOKEN")
STRAVA_REFRESH_TOKEN = os.getenv("STRAVA_REFRESH_TOKEN")
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path("admin/", admin.site.urls),
path("api/auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("api/", include("apps.activities.urls")),
]
from django.contrib import admin
from apps.activities.models.activity import Activity
@admin.register(Activity)
class ActivityAdmin(admin.ModelAdmin):
list_display = (
"name",
"sport_type",
"distance",
"start_date",
"created_at",
)
list_filter = ("sport_type",)
search_fields = ("name",)
ordering = ("-start_date",)
from django.apps import AppConfig
class ActivitiesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.activities"
verbose_name = "Atividades"
from django.urls import path
from rest_framework.routers import DefaultRouter
from apps.activities.views.activity import ActivityViewSet
from apps.activities.views.sync import ActivitySyncView
from apps.activities.views.strava_auth import StravaCallbackView
from apps.activities.views.stats import ActivityStatsView
router = DefaultRouter()
router.register(r"activities", ActivityViewSet, basename="activities")
urlpatterns = [
path("activities/sync/", ActivitySyncView.as_view(), name="activities-sync"),
path("stats/activities/", ActivityStatsView.as_view(), name="activities-stats"),
path("strava/callback/", StravaCallbackView.as_view(), name="strava-callback"),
]
urlpatterns += router.urls
from .activity import Activity
from django.db import models
class Activity(models.Model):
strava_id = models.BigIntegerField(
unique=True,
help_text="ID da atividade no Strava",
)
name = models.CharField(
max_length=255,
help_text="Nome da atividade",
)
distance = models.FloatField(
help_text="Distância em metros",
)
moving_time = models.PositiveIntegerField(
help_text="Tempo em movimento (segundos)",
)
sport_type = models.CharField(
max_length=50,
help_text="Tipo de esporte (Run, Walk, Ride, etc)",
)
start_date = models.DateTimeField(
help_text="Data/hora de início (UTC)",
)
average_speed = models.FloatField(
null=True,
blank=True,
help_text="Velocidade média (m/s)",
)
created_at = models.DateTimeField(
auto_now_add=True,
)
class Meta:
ordering = ["-start_date"]
verbose_name = "Atividade"
verbose_name_plural = "Atividades"
def __str__(self) -> str:
return f"{self.name} - {self.distance / 1000:.2f} km"
from .activity import ActivitySerializer
from rest_framework import serializers
from apps.activities.models.activity import Activity
class ActivitySerializer(serializers.ModelSerializer):
distance_km = serializers.SerializerMethodField()
class Meta:
model = Activity
fields = [
"id",
"strava_id",
"name",
"distance",
"distance_km",
"moving_time",
"sport_type",
"start_date",
"average_speed",
"created_at",
]
def get_distance_km(self, obj: Activity) -> float:
return round(obj.distance / 1000, 2)
from .activity import ActivityFilter
import django_filters
from apps.activities.models.activity import Activity
class ActivityFilter(django_filters.FilterSet):
min_distance = django_filters.NumberFilter(
field_name="distance",
lookup_expr="gte",
help_text="Distância mínima em metros",
)
sport_type = django_filters.CharFilter(
field_name="sport_type",
lookup_expr="iexact",
)
class Meta:
model = Activity
fields = [
"sport_type",
"min_distance",
]
from .activity import ActivityViewSet
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from apps.activities.models.activity import Activity
from apps.activities.serializers.activity import ActivitySerializer
from apps.activities.filters.activity import ActivityFilter
class ActivityViewSet(ReadOnlyModelViewSet):
queryset = Activity.objects.all()
serializer_class = ActivitySerializer
permission_classes = [IsAuthenticated]
filterset_class = ActivityFilter
@action(detail=False, methods=["get"], url_path="long")
def long_activities(self, request):
queryset = self.get_queryset().filter(distance__gt=5000)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=["get"], url_path="walk")
def walk_activities(self, request):
queryset = self.get_queryset().filter(sport_type__iexact="Walk")
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
import requests
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import AllowAny
class StravaCallbackView(APIView):
permission_classes = [AllowAny]
def get(self, request):
code = request.GET.get("code")
if not code:
return Response(
{"detail": "Code não informado"},
status=status.HTTP_400_BAD_REQUEST,
)
token_url = "https://www.strava.com/oauth/token"
payload = {
"client_id": settings.STRAVA_CLIENT_ID,
"client_secret": settings.STRAVA_CLIENT_SECRET,
"code": code,
"grant_type": "authorization_code",
}
response = requests.post(token_url, data=payload)
if response.status_code != 200:
return Response(
{
"detail": "Erro ao autenticar no Strava",
"error": response.text,
},
status=response.status_code,
)
return Response(response.json(), status=status.HTTP_200_OK)
from django.db.models import Count, Sum, Avg
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from apps.activities.models.activity import Activity
class ActivityStatsView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
queryset = Activity.objects.all()
# Filtro opcional por sport_type
sport_type = request.query_params.get("sport_type")
if sport_type:
queryset = queryset.filter(sport_type__iexact=sport_type)
total_activities = queryset.count()
aggregates = queryset.aggregate(
total_distance=Sum("distance"),
average_distance=Avg("distance"),
total_moving_time=Sum("moving_time"),
)
by_sport_type = (
queryset.values("sport_type")
.annotate(total=Count("id"))
.order_by("-total")
)
return Response(
{
"total_activities": total_activities,
"total_distance_km": round((aggregates["total_distance"] or 0) / 1000, 2),
"average_distance_km": round((aggregates["average_distance"] or 0) / 1000, 2),
"total_moving_time_minutes": round((aggregates["total_moving_time"] or 0) / 60, 1),
"by_sport_type": {
item["sport_type"]: item["total"] for item in by_sport_type
},
}
)
from .strava import fetch_activities
import requests
from django.conf import settings
def fetch_activities(page: int = 1, per_page: int = 50) -> list[dict]:
url = "https://www.strava.com/api/v3/athlete/activities"
headers = {
"Authorization": f"Bearer {settings.STRAVA_ACCESS_TOKEN}",
}
params = {
"page": page,
"per_page": per_page,
}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from apps.activities.services.strava import fetch_activities
from apps.activities.models.activity import Activity
class ActivitySyncView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
activities_data = fetch_activities()
created = 0
skipped = 0
for item in activities_data:
activity, was_created = Activity.objects.get_or_create(
strava_id=item["id"],
defaults={
"name": item.get("name"),
"distance": item.get("distance"),
"moving_time": item.get("moving_time"),
"sport_type": item.get("sport_type"),
"start_date": item.get("start_date"),
"average_speed": item.get("average_speed"),
},
)
if was_created:
created += 1
else:
skipped += 1
return Response(
{
"created": created,
"skipped": skipped,
"total": created + skipped,
},
status=status.HTTP_201_CREATED,
)
https://www.strava.com/oauth/authorize
?client_id=SEU_CLIENT_ID
&response_type=code
&redirect_uri=http://localhost:8000/api/strava/callback/
&approval_prompt=force
&scope=read,activity:read_all
Elaborado por Mateus Schwede
ubsocial.github.io