[Python] API Throttling Help Requested

Discussão em 'Python' iniciado por Stack, Junho 26, 2025.

  1. Stack

    Stack Membro Participativo

    Hi I am having trouble with API throttling causing 429 status errors

    I had been using a version of this code in the past and thought I was pulling all data needed, and recently realised that I was missing a lot of data due to throttling

    Was wondering if anyone had advice in this

    The api I am using is from the OpenFDA: https://open.fda.gov/apis/device/event/

    The documentation says there is a limit of 240 requests per minute but that would make this code too slow

    import os, requests, aiohttp, asyncio, pandas as pd, logging, zipfile, io, tkinter as tk
    from tkinter import ttk, messagebox
    from datetime import datetime, timedelta
    import threading, random

    # === Configuration ===
    SAVE_DIR = r"C:FOLDER\FILE\PATH"
    os.makedirs(SAVE_DIR, exist_ok=True)
    CurrentRunName = datetime.now().strftime("%Y%m%d-%H%M%S")
    LOG_PATH = os.path.join(SAVE_DIR, "process_validation.log")
    logging.basicConfig(filename=LOG_PATH, level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

    csv_url = 'https://www.accessdata.fda.gov/MAUDE/ftparea/patientproblemcode.zip'

    # === GUI Setup ===
    Root = tk.Tk()
    Root.title("FDA Data Processor")
    Root.configure(bg='light blue')
    Progress_var = tk.DoubleVar()
    Status_label = ttk.Label(Root, text="Idle")
    Status_label.grid(row=4, columnspan=2, pady=5)

    ttk.Label(Root, text="Start Date (YYYY-MM-DD):").grid(row=0, column=0, padx=10, pady=5)
    Start_date_entry = ttk.Entry(Root)
    Start_date_entry.grid(row=0, column=1, padx=10, pady=5)

    ttk.Label(Root, text="End Date (YYYY-MM-DD):").grid(row=1, column=0, padx=10, pady=5)
    End_date_entry = ttk.Entry(Root)
    End_date_entry.grid(row=1, column=1, padx=10, pady=5)

    Submit_button = ttk.Button(Root, text="Submit")
    Submit_button.grid(row=2, columnspan=2, pady=10)

    Progress_bar = ttk.Progressbar(Root, variable=Progress_var, maximum=100)
    Progress_bar.grid(row=3, columnspan=2, padx=10, pady=10)

    # === Download ZIP ===
    def download_csv_data(retries=3):
    headers = {"User-Agent": "Mozilla/5.0"}
    for attempt in range(1, retries + 1):
    try:
    response = requests.get(csv_url, headers=headers, timeout=20)
    if response.status_code == 200:
    with zipfile.ZipFile(io.BytesIO(response.content)) as z:
    with z.open(z.namelist()[0]) as f:
    return pd.read_csv(f, delimiter='|')
    except Exception as e:
    logging.warning(f"ZIP download error (attempt {attempt}): {e}")
    return pd.DataFrame()

    # === Async Fetch per Day with Logging ===
    async def fetch_day_data(session, date_str, max_retries=3):
    url = f'https://api.fda.gov/device/event.json?search=date_received:[{date_str}+TO+{date_str}]&limit=1000'
    results_df = pd.DataFrame()

    async def get_with_retries(url):
    for attempt in range(1, max_retries + 1):
    try:
    async with session.get(url, headers={"Accept-Encoding": "gzip"}) as response:
    logging.info(f"{date_str}: Attempt {attempt}, HTTP {response.status}")
    if response.status == 404:
    logging.warning(f"{date_str}: No data found (404).")
    return None, 404
    if response.status == 429:
    raw_retry = response.headers.get("Retry-After", "5")
    retry_after = min(int(raw_retry), 30)
    jitter = random.uniform(1, 3)
    logging.warning(f"{date_str}: 429. Capped retry {retry_after}s + jitter {jitter:.1f}s (attempt {attempt})")
    await asyncio.sleep(retry_after + jitter)
    continue
    if response.status != 200:
    logging.warning(f"{date_str}: HTTP {response.status} (attempt {attempt})")
    continue
    return await response.json(), 200
    except Exception as e:
    logging.error(f"{date_str}: Exception on attempt {attempt}: {e}")
    return None, -1

    data, status = await get_with_retries(url)
    if status != 200 or not data:
    logging.error(f"{date_str}: Final status {status}. No data returned.")
    return results_df, -1, 0

    expected = data.get("meta", {}).get("results", {}).get("total", 0)
    results_df = pd.DataFrame(data.get("results", []))
    logging.info(f"{date_str}: Expected {expected}. Page 1 got {len(results_df)}")

    next_link = data.get("Link")
    page_count = 1
    while next_link:
    logging.info(f"{date_str}: Fetching page {page_count + 1}")
    next_data, next_status = await get_with_retries(next_link)
    if not next_data or next_status != 200:
    logging.warning(f"{date_str}: Pagination stopped at page {page_count + 1}")
    break
    more = pd.DataFrame(next_data.get("results", []))
    if not more.empty:
    results_df = pd.concat([results_df, more], ignore_index=True)
    next_link = next_data.get("Link")
    page_count += 1

    logging.info(f"{date_str}: Retrieved {len(results_df)} total records across {page_count} page(s)")
    return results_df, expected, len(results_df)

    # === Batch Async Fetch ===
    async def fetch_date_set(dates, label="Main Run"):
    results_list, daily_summary = [], []
    async with aiohttp.ClientSession() as session:
    for i in range(0, len(dates), 10):
    batch = dates[i:i+10]
    tasks = [fetch_day_data(session, d) for d in batch]
    results = await asyncio.gather(*tasks)
    await asyncio.sleep(2)
    for j, date_str in enumerate(batch):
    try:
    df, expected, retrieved = results[j]
    except Exception as e:
    logging.error(f"{date_str}: Batch error: {e}")
    df, expected, retrieved = pd.DataFrame(), -1, 0
    if not df.empty:
    results_list.append(df)
    status = "Success" if expected == retrieved else ("Partial" if retrieved > 0 else "Failed")
    daily_summary.append({"Date": date_str, "Expected": expected, "Retrieved": retrieved, "Status": status})
    Root.after(0, Progress_var.set, len(daily_summary) / len(dates) * 100)
    return results_list, daily_summary

    # === Main Run Submission ===
    def on_submit():
    try:
    start_date = datetime.strptime(Start_date_entry.get(), '%Y-%m-%d')
    end_date = datetime.strptime(End_date_entry.get(), '%Y-%m-%d')
    Progress_var.set(0)

    def run():
    try:
    Status_label.config(text="⏳ Fetching data...")
    Root.update()
    dates = [(start_date + timedelta(days=i)).strftime('%Y%m%d') for i in range((end_date - start_date).days + 1)]
    all_dfs, all_summary = asyncio.run(fetch_date_set(dates))

    failed_dates = [row["Date"] for row in all_summary if row["Status"] == "Failed"]
    if failed_dates:
    Status_label.config(text=" Retrying failed dates...")
    Root.update()
    retry_dfs, retry_summary = asyncio.run(fetch_date_set(failed_dates, label="Retry"))
    all_dfs.extend(retry_dfs)
    for retry_row in retry_summary:
    for idx, row in enumerate(all_summary):
    if row["Date"] == retry_row["Date"]:
    all_summary[idx] = retry_row
    break

    pd.DataFrame(all_summary).to_csv(os.path.join(SAVE_DIR, f"{CurrentRunName}_daily_summary.csv"), index=False, encoding='utf-8-sig')

    Status_label.config(text="️ Processing ZIP data...")
    Root.update()
    api_df = pd.concat(all_dfs, ignore_index=True) if all_dfs else pd.DataFrame()
    if api_df.empty:
    messagebox.showerror("Error", "No API data retrieved.")
    return

    api_df.columns = api_df.columns.str.lower().str.strip()
    zip_df = download_csv_data()
    if zip_df.empty:
    messagebox.showerror("Error", "ZIP download failed.")
    return
    zip_df.columns = zip_df.columns.str.lower().str.strip()

    if "mdr_report_key" not in api_df.columns or "mdr_report_key" not in zip_df.columns:
    messagebox.showerror("Error", "mdr_report_key column missing in data.")
    return

    api_df["mdr_report_key"] = api_df["mdr_report_key"].astype(str)
    zip_df["mdr_report_key"] = zip_df["mdr_report_key"].astype(str)
    api_keys = set(api_df["mdr_report_key"])
    zip_keys = set(zip_df["mdr_report_key"])
    matched_keys = api_keys & zip_keys
    unmatched_keys = api_keys - zip_keys

    api_df.to_parquet(os.path.join(SAVE_DIR, "api_results.parquet"), index=False)
    filtered_df = zip_df[zip_df["mdr_report_key"].isin(matched_keys)]
    filtered_df.to_parquet(os.path.join(SAVE_DIR, "PatientProblemData.parquet"), index=False)

    with open(os.path.join(SAVE_DIR, "UnmatchedKeys.txt"), "w") as f:
    for key in sorted(unmatched_keys):
    f.write(f"{key}\n")

    Status_label.config(text="✅ Done!")
    Progress_var.set(100)
    messagebox.showinfo("Success", f"Process complete! Files saved to {SAVE_DIR}")

    except Exception as e:
    logging.error(f"Run error: {e}")
    messagebox.showerror("Error", f"Unexpected error during run: {e}")
    Status_label.config(text="❌ Error during processing.")
    Progress_var.set(0)

    threading.Thread(target=run, daemon=True).start()

    except Exception as e:
    logging.error(f"Submission error: {e}")
    messagebox.showerror("Error", f"Invalid input: {e}")
    Status_label.config(text="❌ Invalid input.")
    Progress_var.set(0)

    # === Final Binding ===
    Submit_button.config(command=on_submit)
    Root.mainloop()

    Continue reading...

Compartilhe esta Página