Skip to content

Instantly share code, notes, and snippets.

@marcusmotill
Last active September 12, 2024 19:10
Show Gist options
  • Save marcusmotill/e98b13eedd8aa9cd976e5673b6b746cf to your computer and use it in GitHub Desktop.
Save marcusmotill/e98b13eedd8aa9cd976e5673b6b746cf to your computer and use it in GitHub Desktop.

Teachable Bug Report

Below is documentation and a reproduction script showcasing a bug in the teach pricing_plans api (have not confirmed if the same issue exists in other public APIs).

Issue: Inconsistent results in the '/v1/pricing_plans' list API.

During a project to upload our teachable data to a SQL database for reporting purposes I noticed that some data was missing. Upon further inspection it appears that the list API returns inconsistent results. In the below code, I loop over all pages of the list api multiple times and compare their results. As you will see there is not only missing data between full loops but occasionally duplicate data gets returned within the same loop. Interestingly, the number of results is always the same. I would suspect there may be a missing default sort param yeilding the inconsistent results.

Python Script to reproduce

import requests
from collections import Counter

def api_get(endpoint: str, query_params: dict) -> dict:
    base_url = "https://developers.teachable.com"
    url = base_url + endpoint

    headers = {
        "apiKey": "foobar",
        "Accept": "application/json"
    }
    retries = 0
    while retries < 5:
        r = requests.get(url, params=query_params, headers=headers)
        if r.status_code == 200:
            break
        else:
            retries += 1
    return r.json()

def compare_lists(list_of_lists):
    if not list_of_lists or len(list_of_lists) == 1:
        return True, None
    
    reference_set = set(list_of_lists[0])
    all_same = True
    missing_elements = {}
    extra_elements = {}
    
    for i, lst in enumerate(list_of_lists[1:], start=1):
        current_set = set(lst)
        if current_set != reference_set:
            all_same = False
            missing = reference_set - current_set
            extra = current_set - reference_set
            
            for elem in missing:
                missing_elements.setdefault(elem, []).append(i)
            for elem in extra:
                extra_elements.setdefault(elem, []).append(i)
    
    return all_same, (missing_elements, extra_elements)

def format_differences(missing, extra):
    messages = []
    
    for elem, indices in missing.items():
        indices_str = " and ".join(map(str, indices))
        messages.append(f"List{'s' if len(indices) > 1 else ''} {indices_str} {'are' if len(indices) > 1 else 'is'} missing '{elem}'")
    
    for elem, indices in extra.items():
        indices_str = " and ".join(map(str, indices))
        messages.append(f"List{'s' if len(indices) > 1 else ''} {indices_str} {'have' if len(indices) > 1 else 'has'} extra element '{elem}'")
    
    return messages

pricing_plan_id_map = []

for loop in [0,1,2,3]:
    page = 1
    per = 50
    pricing_plans_count = 0
    number_of_pages = float('inf')
    running_ids = []
    while page <= number_of_pages:
        data = api_get('/v1/pricing_plans', {
            "page": page,
            "per": per,
        })

        current_pricing_plans = data.get('pricing_plans', [])
        meta = data.get('meta', {})
        number_of_pages = meta.get('number_of_pages', 0)

        pricing_plans_count += len(current_pricing_plans)

        running_ids.extend([pricing_plan['id'] for pricing_plan in current_pricing_plans])

        page += 1
    
    pricing_plan_id_map.append(running_ids)

    print(f"Fetched {pricing_plans_count} items in loop {loop}")


are_same, differences = compare_lists(pricing_plan_id_map)

if are_same:
    print("All lists have the same elements.")
else:
    missing, extra = differences
    messages = format_differences(missing, extra)
    print("Differences found:")
    for message in messages:
        print(message)

Example output

Differences found:
List 1 is missing '3339009'
List 1 is missing '4195586'
List 1 is missing '4458760'
List 1 is missing '4485517'
List 1 is missing '3728274'
List 1 is missing '4494329'
List 1 is missing '4239130'
List 1 is missing '4185371'
List 1 is missing '4859036'
List 1 is missing '3711645'
List 1 is missing '3711646'
List 1 is missing '3653531'
List 1 is missing '4195488'
List 1 is missing '3812640'
List 1 is missing '3794845'
List 1 is missing '3676579'
List 1 is missing '3676580'
List 1 is missing '3653533'
List 1 is missing '4494377'
List 1 is missing '3907241'
List 1 is missing '3728301'
List 1 is missing '4077437'
List 1 is missing '4077438'
List 1 is missing '3694524'
List 1 is missing '4125124'
List 1 is missing '4151621'
List 1 is missing '4180804'
List 1 is missing '4163787'
List 1 is missing '3750731'
List 1 is missing '3750732'
List 1 is missing '3812685'
List 1 is missing '3435731'
List 1 is missing '3694549'
List 1 is missing '3728216'
List 1 is missing '3649377'
List 1 is missing '3676642'
List 1 is missing '4571747'
List 1 is missing '4600548'
List 1 is missing '3650405'
List 1 is missing '4137318'
List 1 is missing '4932841'
List 1 is missing '3661673'
List 1 is missing '4530932'
List 1 is missing '4125173'
List 1 is missing '4905972'
List 1 is missing '3605368'
List 1 is missing '4950905'
List 1 is missing '4950909'
List 1 is missing '5239422'
List 1 has extra element '5201928'
List 1 has extra element '5201929'
List 1 has extra element '4976650'
List 1 has extra element '5614096'
List 1 has extra element '4976658'
List 1 has extra element '4616724'
List 1 has extra element '4616725'
List 1 has extra element '4906007'
List 1 has extra element '4906009'
List 1 has extra element '5338650'
List 1 has extra element '4879907'
List 1 has extra element '4879908'
List 1 has extra element '4485165'
List 1 has extra element '5654576'
List 1 has extra element '4894263'
List 1 has extra element '4894264'
List 1 has extra element '4879954'
List 1 has extra element '5614167'
List 1 has extra element '5614168'
List 1 has extra element '4894296'
List 1 has extra element '5237851'
List 1 has extra element '4818535'
List 1 has extra element '4879980'
List 1 has extra element '4916340'
List 1 has extra element '4916342'
List 1 has extra element '5363832'
List 1 has extra element '5237883'
List 1 has extra element '5239421'
List 1 has extra element '5610627'
List 1 has extra element '5239429'
List 1 has extra element '4880007'
List 1 has extra element '4880008'
List 1 has extra element '4880010'
List 1 has extra element '5363859'
List 1 has extra element '5308054'
List 1 has extra element '5308055'
List 1 has extra element '4843672'
List 1 has extra element '4843673'
List 1 has extra element '3711643'
List 1 has extra element '5418655'
List 1 has extra element '5418660'
List 1 has extra element '5363877'
List 1 has extra element '5418661'
List 1 has extra element '5083815'
List 1 has extra element '5083816'
List 1 has extra element '4843698'
List 1 has extra element '4843699'
List 1 has extra element '4859061'
List 1 has extra element '4541621'
List 1 has extra element '4859062'
List 1 has extra element '4859064'
List 1 has extra element '5418679'
List 1 has extra element '4204732'
List 1 has extra element '4092099'
List 1 has extra element '5263045'
List 1 has extra element '5610699'
List 1 has extra element '4218060'
List 1 has extra element '4859084'
List 1 has extra element '4859085'
List 1 has extra element '4859087'
List 1 has extra element '5617880'
List 1 has extra element '4954329'
List 1 has extra element '4954328'
List 1 has extra element '4195548'
List 1 has extra element '5219047'
List 1 has extra element '5219050'
List 1 has extra element '5219051'
List 1 has extra element '5219053'
List 1 has extra element '5087988'
List 1 has extra element '4485365'
List 1 has extra element '4530933'
List 1 has extra element '4918007'
List 1 has extra element '5553912'
List 1 has extra element '5168378'
List 1 has extra element '5553914'
List 1 has extra element '4920064'
List 1 has extra element '4530953'
List 1 has extra element '5341459'
List 1 has extra element '5384983'
List 1 has extra element '5384984'
List 1 has extra element '5230359'
List 1 has extra element '5230362'
List 1 has extra element '4530974'
List 1 has extra element '4530975'
List 1 has extra element '5609760'
List 1 has extra element '4180767'
List 1 has extra element '5341476'
List 1 has extra element '5486373'
List 1 has extra element '4137267'
List 1 has extra element '4866369'
List 1 has extra element '4866370'
List 1 has extra element '4180807'
List 1 has extra element '4397392'
List 1 has extra element '5301588'
List 1 has extra element '4471641'
List 1 has extra element '4629342'
List 1 has extra element '4866399'
List 1 has extra element '4629343'
List 1 has extra element '4866401'
List 1 has extra element '4866402'
List 1 has extra element '4566525'
List 1 has extra element '4137317'
List 1 has extra element '3661672'
List 1 has extra element '5089128'
List 1 has extra element '4238187'
List 1 has extra element '4238188'
List 1 has extra element '4629365'
List 1 has extra element '5599611'
List 1 has extra element '5609852'
List 1 has extra element '5609853'
List 1 has extra element '4508543'
List 1 has extra element '4238212'
List 1 has extra element '4238213'
List 1 has extra element '5609870'
List 1 has extra element '4910990'
List 1 has extra element '4866464'
List 1 has extra element '4866465'
List 1 has extra element '5614006'
List 1 has extra element '5614007'
List 1 has extra element '5357510'
List 1 has extra element '5357515'
List 1 has extra element '4566476'
List 1 has extra element '5357516'
List 1 has extra element '5201887'
List 1 has extra element '3676641'
List 1 has extra element '4215276'
List 1 has extra element '4616687'
List 1 has extra element '4905971'
List 1 has extra element '5643252'
List 1 has extra element '5643251'
List 1 has extra element '5615604'
List 1 has extra element '4494328'
List 1 has extra element '5614076'
List 1 has extra element '5614077'
List 1 has extra element '4566526'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment