Faster Development with ChatGPT: Peloton workout metrics in iCalendar

Introduction

Are you a software developer who loves your Peloton workouts but struggles to keep track of them in your calendar? Look no further! In this blog post, we will guide you through the process of seamlessly integrating your Peloton workouts into your calendar using Python.

We’ll walk you through the steps we took to solve this problem, leveraging the power of ChatGPT—a powerful AI language model—to assist us in finding the necessary Python libraries, implementing the code, and contributing to the open-source community.

Let’s dive in and discover how you can effortlessly sync your Peloton workouts with your calendar, ensuring you never miss a beat.

Identifying the Problem: Syncing Peloton Workouts with Your Calendar

As a software developer and a Peloton enthusiast, you understand the importance of keeping your workout schedule organized. However, manually adding your Peloton workouts to your calendar can be tedious and time-consuming. You’ve wondered if there’s a way to automate this process and have your Peloton workouts seamlessly integrated with your calendar application of choice.

In your search for a solution, you turned to ChatGPT, a powerful AI language model, to help you navigate the vast landscape of available resources, libraries, and coding techniques. By asking targeted questions and receiving expert guidance from ChatGPT, you were able to streamline the development process and find the optimal solution.

In this blog post, we’ll share the journey of how we leveraged ChatGPT’s assistance to tackle this problem head-on. We’ll walk you through each step, from identifying the necessary Python libraries to implementing the code and contributing to the open-source community. By the end, you’ll see how we leveraged ChatGPT to create a new command-line tool to sync your Peloton workouts with your calendar with much less effort.

Let’s get started on this exciting adventure of integrating Peloton workouts into your calendar!

A Series of ChatGPT Prompts

The first step was to figure out the design and what existing tools we could leverage. So, we asked ChatGPT:

I would like to create a project that retrieves my exercise & health data from Peloton and creates ICS iCalendar data to summarize each workout. This can be in any of Java, Go, PHP or Python… whichever has the best support for retrieving data from Peloton and converting it to ICS. Give me a top-level design.

That gave us some options. However, the solution focused on having a Peloton API key. And, at the time of this writing, the Peloton API site did not seem to be working properly. We need a solution that uses your username and password instead. After some more questions for ChatGPT, we settled on a python module called pylotoncycle, which is available on GitHub.

Researching Python Libraries: Finding the Right Tools for the Job

Next, we wanted to know what other tools we could leverage to make development fast for our command line tool. We prompted ChatGPT with “What 3rd party libraries are available to assist in development”. This is what we got back:

For Peloton, you can use the pypelotoncycle library, which provides a Python interface to Peloton’s API. This library allows you to authenticate with Peloton, retrieve workout data, and perform other actions through Python code.

For iCalendar, Python has the icalendar library, which provides functionality for creating and parsing iCalendar data. This library allows you to create iCalendar events with properties such as start and end times, location, summary, description, and more.

Python is also a great choice for command-line tools, as it provides robust support for command-line argument parsing and handling through the built-in argparse module.

Overall, Python has strong third-party library support for both Peloton and iCalendar, and it provides a powerful and flexible language for building command-line tools.

Implementing the Solution: Converting Peloton Workouts to iCalendar Format

Now that we have identified the appropriate Python libraries, it’s time to put them into action and convert Peloton workouts into the iCalendar format. We started with a simple ChatGPT prompt:

Armed with the knowledge gained from ChatGPT and our development expertise, we set out to implement the solution.

We created a Python script, workouts_to_ics.py, that utilizes the pylotoncycle and icalendar libraries to fetch workout data from Peloton and convert it into iCalendar events. Let’s explore the implementation details:

  1. Fetching Peloton Workout Data: We leveraged the pylotoncycle library to retrieve workout information from Peloton. With functions and classes provided by the library, we accessed data such as ride titles, durations, instructors, and more.
  2. Creating iCalendar Events: Using the fetched workout data, we utilized the icalendar library to create iCalendar events. We set attributes like the event’s title, start and end times, and description. The library enabled us to generate valid iCalendar data that can be imported into popular calendar applications.
  3. Generating the iCalendar File: After creating the iCalendar events, we assembled them into an iCalendar file using the capabilities of the icalendar library. The resulting file contained all the converted Peloton workouts in a format that can be easily imported into various calendar applications.

With the workouts_to_ics.py script, we achieved the core objective of converting Peloton workouts to the iCalendar format. However, our journey didn’t stop there. We wanted to ensure the robustness and reliability of our solution by testing the code.

Testing the Solution: Ensuring Accuracy and Reliability

To ensure the accuracy and reliability of our solution, we implemented a comprehensive testing strategy. Testing plays a crucial role in software development, as it helps identify and prevent potential issues, ensuring that the code functions as intended. Let’s dive into the testing process we followed for our Peloton workouts to iCalendar conversion script. Our general workflow was to provide a ChatGPT prompt like “Please provide unit tests for the following function” with our python code pasted in. I would end the prompt with “And please use the following workout data for your unit tests but remove unused fields in the data”, and I pasted in the JSON representation of a workout.

  1. Unit Testing with unittest: We leveraged the unittest framework in Python to create a suite of unit tests. These tests targeted different aspects of the code, verifying its behavior under various scenarios. We focused on testing key functions, such as generate_description() and convert_to_ical(), to validate their correctness.
  2. Test Cases for Various Scenarios: We designed test cases that covered different types of Peloton workouts, including regular rides and “Just Ride” sessions. Each test case included input workout data and an expected output, allowing us to compare the actual results with the expected ones.
  3. Assertion-Based Testing: Within each test case, we utilized assertion statements to check if the generated iCalendar data matched our expectations. By comparing the actual and expected values, we could quickly identify any discrepancies and pinpoint potential issues.
  4. Handling Edge Cases and Exceptions: We also considered edge cases and exceptional scenarios to ensure the code handled them gracefully. For example, we tested the behavior when workout data was missing or when certain fields were empty. This helped us catch potential bugs and improve the overall robustness of our solution.

By conducting thorough unit testing, we could confidently verify that our Peloton workouts to iCalendar conversion script performed as expected. We aimed to cover a wide range of scenarios and data variations to ensure the reliability and accuracy of the code. See the test code here in test_workouts_to_ics.py.

Contributing to the pylotoncycle Library: Collaborating for the Community

As developers, we understand the power of open-source collaboration and the benefits it brings to the entire community. While working on our Peloton workouts to iCalendar conversion script, we identified an opportunity to contribute to the pylotoncycle library, which we used as a foundation for our project. By sharing our improvements and enhancements, we could help other developers streamline their Peloton integration workflows.

  1. Identifying the Contribution: We recognized that our code enhancements, including the workouts_to_ics.py script and the corresponding unit tests, could be valuable additions to the pylotoncycle library. These additions provided a new feature and improved the overall functionality, making it easier for developers to convert Peloton workouts to iCalendar format.
  2. Creating a Pull Request: To contribute our code changes to the pylotoncycle library, we followed the standard open-source practice of creating a pull request. We forked the original repository, added our modified files, and submitted a pull request to the project’s maintainers. This allowed us to share our work with the community and initiate a discussion around the proposed changes. ChatGPT is helpful here with a simple prompt: “I would like to make local changes to a project on GitHub and then submit the changes as a pull request. Show me the steps and commands to do that.”

You can find our fork of the code here on Github.

Appendix: The code for workouts_to_ics.py

#!/usr/bin/env python3

"""
workouts_to_ics.py
Convert Peloton workouts to iCalendar (RFC 2445) format.

This script utilizes the pylotoncycle library to fetch recent workouts and
outputs them to an ICS file that can be imported into calendar applications.

Example usage:
    python workouts_to_ics.py --username <username> --password <password>

Dependencies:
    - pylotoncycle
    - icalendar

"""
import os
import sys
import traceback
import argparse
import json
from datetime import datetime, timedelta
from icalendar import Calendar, Event
import pylotoncycle

def parse_command_line():
    """
    Parse command-line arguments.

    Returns:
        argparse.Namespace: An object containing parsed command-line arguments.

    """
    parser = argparse.ArgumentParser()
    parser.add_argument('--username', help='Peloton username')
    parser.add_argument('--password', help='Peloton password')
    parser.add_argument('--num_results', type=int, default=10, help='Number of recent workouts to fetch')
    parser.add_argument('--input_json', help='Path to input JSON file containing workouts')
    parser.add_argument('--output_json', help='Path to output JSON file for the fetched workouts')
    parser.add_argument('--calendar_name', default='Peloton Workouts', help='Calendar name')
    parser.add_argument('--debug', action='store_true', help='Enable debug mode')
    return parser.parse_args()


def generate_description(workout, silent=False):
    """
    Generate the description for a workout.

    Args:
        workout (dict): The workout data.

    Returns:
        tuple: A tuple containing the generated description (str) and the error (Exception) if any.

    """
    ride = workout['ride']
    leaderboard_rank = workout.get('leaderboard_rank', 'N/A')
    total_leaderboard_users = workout.get('total_leaderboard_users', 'N/A')
    ftp = workout['ftp_info'].get('ftp', 'N/A')

    instructor_name = workout['instructor_name']
    try:
        if instructor_name == "JUST RIDE":
            workout_len = workout['ride']['duration'] // 60
            description = f"{workout_len} minute Just Ride"
        else:
            description = f"Ride Title: {ride.get('title', 'N/A')}\n"
            description += f"Difficulty: {ride.get('difficulty_estimate', 'N/A')}\n"
            if leaderboard_rank != 0 and total_leaderboard_users:
                percentile = leaderboard_rank / total_leaderboard_users * 100
                description += f"Leaderboard: {leaderboard_rank}/{total_leaderboard_users} {percentile:.2f}%\n"
            description += f"FTP: {ftp}\n"
            description += f"Description: {ride.get('description', 'N/A')}"
        return description, None
    except Exception as err:
        if not silent:
            print(f"Error: {err}", file=sys.stderr)
            traceback.print_exc()
        return "Unknown Description", err


def convert_to_ical(workouts, calendar_name='Peloton Workouts'):
    """
    Convert workouts to iCalendar format.

    Args:
        workouts (list): List of workout data.
        calendar_name (str, optional): Name of the calendar. Defaults to 'Peloton Workouts'.

    Returns:
        str: The generated iCalendar data.

    """
    cal = Calendar()
    cal.add('VERSION', '2.0')
    cal.add('PRODID', '-//pylotoncycle workouts_to_ics.py//EN')
    cal.add('X-WR-CALNAME', calendar_name)

    for workout in workouts:
        created_at = datetime.fromtimestamp(int(workout['created_at']))
        end_time = created_at + timedelta(minutes=workout['ride']['duration'] // 60)
        workout_type = workout['fitness_discipline']
        ride = workout['ride']
        workout_title = ride.get('title', 'Untitled')
        instructor_name = workout['instructor_name']
        if instructor_name == "JUST RIDE":
            workout_len = workout['ride']['duration'] // 60
            title = f"{workout_len} minute Just Ride"
        else:
            title = f"{workout_title} with {instructor_name}"

        start_time = created_at
        end_time = created_at + timedelta(minutes=workout['ride']['duration'] // 60)

        event = Event()
        # Add image as ATTACH if there is one
        image_url = ride.get('image_url', None)
        if image_url:
            event.add('attach', image_url)
        event.add('summary', title.encode('utf-8'))
        event.add('dtstart', start_time)
        event.add('dtend', end_time)
        description, err = generate_description(workout)
        event.add('description', description.encode('utf-8'))

        cal.add_component(event)

    ical_data = cal.to_ical().decode('utf-8')

    return ical_data


if __name__ == "__main__":
    args = parse_command_line()
    debug_mode = args.debug

    username = args.username or os.environ.get('PELOTON_USERNAME')
    password = args.password or os.environ.get('PELOTON_PASSWORD')
    input_json = args.input_json
    output_json = args.output_json

    if input_json:
        with open(input_json) as f:
            workouts = json.load(f)
    else:
        if not username or not password:
            raise ValueError("Peloton username and password are required.")
        print("Attempting to connect...", file=sys.stderr)
        conn = pylotoncycle.PylotonCycle(username, password)
        print("Connected.", file=sys.stderr)
        workouts = conn.GetRecentWorkouts(num_workouts=args.num_results)

    if output_json:
        with open(output_json, 'w') as f:
            json.dump(workouts, f, indent=4)

    ical_data = convert_to_ical(workouts, calendar_name=args.calendar_name)

    # Print the generated iCalendar data to stdout
    print(ical_data)

Leave a Reply

Your email address will not be published. Required fields are marked *