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.
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)
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'