Consumir API Strava com Django REST Framework

Como consumir API Strava com Django REST Framework
Voltar

Material complementar:


Objetivo:

Criar API REST Django REST Framework para consumir API do Strava, listando atividades do usuário.

  1. Criar app de API no Strava:
    • Criar app no Strava: https://www.strava.com/settings/api
    • Nome do aplicativo: Meu aplicativo de atividades
    • Categoria: Análise do Desempenho
    • Clube: [none]
    • Website: http://localhost
    • Descrição: Aplicativo para consumir API do Strava
    • Domínio de autorização callback: localhost
    • Criar (guardar ID cliente, segredo do cliente, token de acesso e token de atualização)
  2. Criar secret key: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
    • Guardar secret key
  3. Criar projeto DRF:
    
    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 ..
    
  4. strava_drf/.env:
    
    
    # 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
    
  5. strava_drf/config/settings.py:
    
    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")
    
  6. strava_drf/config/urls.py:
    
    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")),
    ]
    
  7. strava_drf/apps/activities/admin.py:
    
    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",)
    
  8. strava_drf/apps/activities/apps.py:
    
    from django.apps import AppConfig
    
    class ActivitiesConfig(AppConfig):
        default_auto_field = "django.db.models.BigAutoField"
        name = "apps.activities"
        verbose_name = "Atividades"
    
  9. strava_drf/apps/activities/urls.py:
    
    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
    
  10. strava_drf/apps/activities/models/__init__.py:
    
    from .activity import Activity
    
  11. strava_drf/apps/activities/models/activity.py:
    
    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"
    
  12. strava_drf/apps/activities/serializers/__init__.py:
    
    from .activity import ActivitySerializer
    
  13. strava_drf/apps/activities/serializers/activity.py:
    
    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)
    
  14. strava_drf/apps/activities/filters/__init__.py:
    
    from .activity import ActivityFilter
    
  15. strava_drf/apps/activities/filters/activity.py:
    
    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",
            ]
    
  16. strava_drf/apps/activities/views/__init__.py:
    
    from .activity import ActivityViewSet
    
  17. strava_drf/apps/activities/views/activity.py:
    
    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)
    
  18. strava_drf/apps/activities/views/strava_auth.py:
    
    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)
    
  19. strava_drf/apps/activities/views/stats.py:
    
    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
                    },
                }
            )
    
  20. strava_drf/apps/activities/services/__init__.py:
    
    from .strava import fetch_activities
    
  21. strava_drf/apps/activities/services/strava.py:
    
    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()
    
  22. strava_drf/apps/activities/views/sync.py:
    
    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,
            )
    
  23. Construir migrations: python manage.py makemigrations
  24. Executar migrations: python manage.py migrate
  25. Criar usuário Django admin: python manage.py createsuperuser
  26. Executar servidor: python manage.py runserver
  27. Obter JWT (request):
    • Método: POST
    • URL: http://localhost:8000/api/auth/token/
    • Body (raw JSON): {"username": "seu_usuario_django", "password": "sua_senha_django"}
    • Send (Copiar tokens)
  28. Autorizar API Strava (copiar tokens retornados, e colá-los no .env):
    
    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
    
  29. Executar servidor: python manage.py runserver
  30. Executar sync (request):
    • Método: POST
    • URL: http://localhost:8000/api/activities/sync/
    • Authorization:
      • Type: Bearer Token
      • Token: <access_token_jwt_gerado>
    • Send (Armazenará atividades no banco de dados)
  31. Listar atividades (request):
    • Método: GET
    • URL: http://localhost:8000/api/activities/
    • Authorization:
      • Type: Bearer Token
      • Token: <access_token_jwt_gerado>
    • Send
  32. Listar atividades com distância maior que 5km (request):
    • Método: GET
    • URL: http://localhost:8000/api/activities/long/
    • Authorization:
      • Type: Bearer Token
      • Token: <access_token_jwt_gerado>
    • Send
  33. Listar atividades de caminhada (request):
    • Método: GET
    • URL: http://localhost:8000/api/activities/walk/
    • Authorization:
      • Type: Bearer Token
      • Token: <access_token_jwt_gerado>
    • Send
  34. Listar atividades de caminhada (request REST filter):
    • Método: GET
    • URL: http://localhost:8000/api/activities/?sport_type=Walk
    • Authorization:
      • Type: Bearer Token
      • Token: <access_token_jwt_gerado>
    • Send
  35. Listar estatísticas de atividades (request):
    • Método: GET
    • URL: http://localhost:8000/api/stats/activities/
    • Authorization:
      • Type: Bearer Token
      • Token: <access_token_jwt_gerado>
    • Send
  36. Listar estatísticas de atividades Walk (request):
    • Método: GET
    • URL: http://localhost:8000/api/stats/activities/?sport_type=Walk
    • Authorization:
      • Type: Bearer Token
      • Token: <access_token_jwt_gerado>
    • Send

Elaborado por Mateus Schwede
ubsocial.github.io