125 lines
4.8 KiB
Python
125 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""gumroad-cli — interface with the Gumroad API v2 (stdlib only).
|
|
|
|
Commands:
|
|
products list all products (id, name, price, url)
|
|
product <id> detail for one product
|
|
sales list recent sales (email, product, amount, date)
|
|
followers list follower emails (GET /v2/followers — 404 on current API)
|
|
offer-codes list offer codes per product
|
|
buyers-csv export all unique buyer emails to CSV
|
|
Token: env GUMROAD_TOKEN or --token flag.
|
|
"""
|
|
import argparse, csv, json, os, sys, time, urllib.parse, urllib.request
|
|
|
|
API = "https://api.gumroad.com/v2"
|
|
|
|
def api_get(path, params, token):
|
|
params["access_token"] = token
|
|
url = API + path + "?" + urllib.parse.urlencode(params)
|
|
try:
|
|
with urllib.request.urlopen(url, timeout=30) as r:
|
|
return json.loads(r.read().decode())
|
|
except urllib.error.HTTPError as e:
|
|
return {"http_error": e.code, "body": e.read().decode()[:500]}
|
|
|
|
def api_post(path, params, token):
|
|
params["access_token"] = token
|
|
data = urllib.parse.urlencode(params).encode()
|
|
req = urllib.request.Request(API + path, data=data, method="POST")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
return json.loads(r.read().decode())
|
|
except urllib.error.HTTPError as e:
|
|
return {"http_error": e.code, "body": e.read().decode()[:500]}
|
|
|
|
def money(cents):
|
|
return f"${cents/100:.2f}" if cents is not None else "-"
|
|
|
|
def cmd_products(d, t):
|
|
r = api_get("/products", {}, t)
|
|
for p in r.get("products", []):
|
|
print(f"{p.get('id')}\t{money(p.get('price_cents'))}\t{p.get('name')}\t{p.get('url')}")
|
|
|
|
def cmd_product(d, t):
|
|
r = api_get(f"/products/{d.id}", {}, t)
|
|
p = r.get("product", {})
|
|
if not p:
|
|
print(json.dumps(r, indent=2)[:500]); return
|
|
print(f"ID: {p.get('id')}")
|
|
print(f"Name: {p.get('name')}")
|
|
print(f"Price: {money(p.get('price_cents'))}")
|
|
print(f"URL: {p.get('url')}")
|
|
print(f"Desc: {(p.get('description') or '')[:400]}")
|
|
print(f"Sales: {p.get('sales_count')}")
|
|
print(f"Custom fields: {[f.get('name') for f in p.get('custom_fields') or []]}")
|
|
|
|
def cmd_sales(d, t):
|
|
r = api_get("/sales", {}, t)
|
|
for s in r.get("sales", []):
|
|
print(f"{s.get('created_at','')}\t{s.get('email','')}\t{s.get('product_name','')}\t{money(s.get('amount_cents'))}")
|
|
|
|
def cmd_followers(d, t):
|
|
r = api_get("/followers", {}, t)
|
|
if "http_error" in r:
|
|
print(f"ERR {r['http_error']}: {r['body'][:200]}")
|
|
print("(Gumroad API v2 has no followers endpoint — followers exist only in the dashboard UI)")
|
|
return
|
|
for f in r.get("followers", []):
|
|
print(f.get("email", ""))
|
|
|
|
def cmd_offer_codes(d, t):
|
|
r = api_get("/products", {}, t)
|
|
for p in r.get("products", []):
|
|
pid = p.get("id")
|
|
r2 = api_get(f"/products/{pid}/offer_codes", {}, t)
|
|
codes = r2.get("offer_codes", [])
|
|
if codes:
|
|
print(f"== {p.get('name')} ({pid})")
|
|
for c in codes:
|
|
amount = money(c.get("amount_cents")) if c.get("amount_cents") else f"{c.get('percent_off')}%"
|
|
print(f" {c.get('code')}\t{amount}\t{c.get('name')}")
|
|
|
|
def cmd_buyers_csv(d, t):
|
|
emails = {}
|
|
page, total = 1, None
|
|
while True:
|
|
r = api_get("/sales", {"page": page, "per_page": 100}, t)
|
|
sales = r.get("sales", [])
|
|
for s in sales:
|
|
if s.get("email"):
|
|
emails[s["email"].strip().lower()] = True
|
|
total = r.get("total")
|
|
if not sales or (total is not None and page * 100 >= total) or page > 50:
|
|
break
|
|
page += 1
|
|
out = d.out or "gumroad_buyers.csv"
|
|
with open(out, "w", newline="") as f:
|
|
w = csv.writer(f)
|
|
w.writerow(["email"])
|
|
for e in sorted(emails):
|
|
w.writerow([e])
|
|
print(f"Wrote {len(emails)} unique buyer emails -> {out}")
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(prog="gumroad-cli", description="Gumroad API v2 CLI")
|
|
ap.add_argument("--token", default=os.environ.get("GUMROAD_TOKEN"), help="API token (env GUMROAD_TOKEN)")
|
|
ap.add_argument("--out", default=None, help="output file for buyers-csv")
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
sub.add_parser("products")
|
|
sp = sub.add_parser("product"); sp.add_argument("id")
|
|
sub.add_parser("sales")
|
|
sub.add_parser("followers")
|
|
sub.add_parser("offer-codes")
|
|
bp = sub.add_parser("buyers-csv"); bp.add_argument("--out", default=None)
|
|
d = ap.parse_args()
|
|
t = d.token
|
|
if not t:
|
|
sys.exit("ERROR: no token. Set GUMROAD_TOKEN env or pass --token.")
|
|
{"products": cmd_products, "product": cmd_product, "sales": cmd_sales,
|
|
"followers": cmd_followers, "offer-codes": cmd_offer_codes,
|
|
"buyers-csv": cmd_buyers_csv}[d.cmd](d, t)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|