diff --git a/.gitignore b/.gitignore index 8e0855fa..5a6d3955 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ *.png +output diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..32517883 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +output/sponsors.html: output/sponsors.md + markdown2 $< -x tables > $@ + +output/sponsors.md: analyze_sponsors.py + python3 analyze_sponsors.py > $@ diff --git a/analyze_sponsors.py b/analyze_sponsors.py new file mode 100644 index 00000000..bb20e816 --- /dev/null +++ b/analyze_sponsors.py @@ -0,0 +1,184 @@ +""" +Analyze sponsor data to generate a sponsors table on the website +""" + +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone +from typing import Optional + +import pandas as pd + +Amount = float +Currency = str + + +@dataclass +class Sponsor: + name: str + github_username: Optional[str] = None + donated: list[tuple[Amount, Currency, datetime]] = field(default_factory=list) + source: str = "unknown" + + def __repr__(self): + return f"" + + @property + def total_donated(self): + # group donated by currency, aggregate by sum + currencies = set([currency for _, currency, _ in self.donated]) + total_donated = {} + for currency in currencies: + total_donated[currency] = sum( + [amount for amount, c, _ in self.donated if c == currency] + ) + return total_donated + + +def load_github_sponsors_csv(filename: str) -> list[Sponsor]: + """The CSV looks like the following: + + Sponsor Handle,Sponsor Profile Name,Sponsor Public Email,Sponsorship Started On,Is Public?,Is Yearly?,Transaction ID,Tier Name,Tier Monthly Amount,Processed Amount,Is Prorated?,Status,Transaction Date,Metadata,Country,Region,VAT + snehal-shekatkar,Snehal Shekatkar,,2023-05-07 21:45:50 +0200,true,false,ch_3N5DWnEQsq43iHhX1Svht2g7,$5 a month,$5.00,$4.52,true,settled,2023-05-07 21:46:01 +0200,"",AUT,undefined, + justyn,Justyn Butler,,2023-02-15 12:14:28 +0100,true,false,ch_3NExsXEQsq43iHhX0Fmfcg6w,$3 a month,$3.00,$3.00,false,settled,2023-06-03 19:04:45 +0200,"",GBR,Kent, + justyn,Justyn Butler,,2023-02-15 12:14:28 +0100,true,false,ch_3N3ikOEQsq43iHhX1eMXefVn,$3 a month,$3.00,$3.00,false,settled,2023-05-03 18:41:56 +0200,"",GBR,Kent, + """ + + df = pd.read_csv( + filename, parse_dates=["Sponsorship Started On", "Transaction Date"] + ) + df = df[df["Is Public?"] == True] # noqa: E712 + df = df[df["Status"] == "settled"] + + sponsors = [] + handles = df["Sponsor Handle"].unique() + for handle in handles: + sponsor = Sponsor( + name="placeholder", + github_username=handle, + donated=[], + source="github", + ) + for _, row in df.iterrows(): + if row["Sponsor Handle"] != handle: + continue + sponsor.name = row["Sponsor Profile Name"] + sponsor.donated.append( + ( + float(row["Processed Amount"].replace("$", "")), + "USD", + row["Transaction Date"], + ) + ) + + sponsors.append(sponsor) + + return sponsors + + +def load_opencollective_csv(filename: str) -> list[Sponsor]: + """The CSV looks like this: + + "datetime","shortId","shortGroup","description","type","kind","isRefund","isRefunded","shortRefundId","displayAmount","amount","paymentProcessorFee","netAmount","balance","currency","accountSlug","accountName","oppositeAccountSlug","oppositeAccountName","paymentMethodService","paymentMethodType","expenseType","expenseTags","payoutMethodType","merchantId","orderMemo" + "2023-06-06T05:47:17","d3f98b95","f2238225","Contribution from Martin","CREDIT","CONTRIBUTION","","","","$50.00 USD",50,-2.5,47.5,3059.14,"USD","activitywatch","ActivityWatch","guest-cf5e5bf5","Martin","STRIPE","CREDITCARD",,"",,, + "2023-06-06T05:47:17","e3c0d1f9","f2238225","Host Fee to Open Source Collective","DEBIT","HOST_FEE","","","","-$5.00 USD",-5,0,-5,3011.64,"USD","activitywatch","ActivityWatch","opensource","Open Source Collective",,,,"",,, + "2023-06-01T04:03:23","80120e4e","9a189bcd","Yearly contribution from Olli Nevalainen","CREDIT","CONTRIBUTION","","","","$24.00 USD",24,-1.24,22.76,3016.64,"USD","activitywatch","ActivityWatch","olli-nevalainen","Olli Nevalainen","STRIPE","CREDITCARD",,"",,, + """ + + df = pd.read_csv(filename, parse_dates=["datetime"]) + df = df[df["type"] == "CREDIT"] + + sponsors = [] + handles = df["oppositeAccountSlug"].unique() + for handle in handles: + sponsor = Sponsor( + name="placeholder", + github_username=handle, + donated=[], + source="opencollective", + ) + for _, row in df.iterrows(): + if row["oppositeAccountSlug"] != handle: + continue + sponsor.name = row["oppositeAccountName"] + sponsor.donated.append( + ( + row["amount"], + row["currency"], + row["datetime"].replace(tzinfo=timezone.utc), + ) + ) + + sponsors.append(sponsor) + + # remove 'GitHub Sponsors' from sponsors + sponsors = [sponsor for sponsor in sponsors if sponsor.name != "GitHub Sponsors"] + + # subtract $3002 from Kerkko (actually FUUG.fi) + for sponsor in sponsors: + if sponsor.name == "Kerkko Pelttari": + sponsor.donated.append((-3002, "USD", datetime.now(tz=timezone.utc))) + + return sponsors + + +def load_patreon_csv(filename: str) -> list[Sponsor]: + """The CSV looks like: + + Name,Email,Twitter,Discord,Patron Status,Follows You,Lifetime Amount,Pledge Amount,Charge Frequency,Tier,Addressee,Street,City,State,Zip,Country,Phone,Patronage Since Date,Last Charge Date,Last Charge Status,Additional Details,User ID,Last Updated,Currency,Max Posts,Access Expiration,Next Charge Date + Karan singh,karan8q@gmail.com,,kraft#9466,Declined patron,No,0.00,1.00,monthly,Time Tracker,,,,,,,,2023-04-08 17:45:28.731953,2023-05-31 07:46:37,Declined,,76783024,2023-05-31 08:01:37.517184,USD,,,2023-05-01 07:00:00 + Dan Thompson,danielrthompsonjr@gmail.com,,,Declined patron,No,0.00,1.00,monthly,Time Tracker,,,,,,,,2023-02-28 20:00:03.756241,2023-03-15 12:03:01,Declined,,46598895,2023-03-15 12:18:01.633900,USD,,,2023-03-01 08:00:00 + """ + + df = pd.read_csv(filename, parse_dates=["Patronage Since Date", "Last Charge Date"]) + + sponsors = [] + for _, row in df.iterrows(): + sponsor = Sponsor( + name=row["Name"], + github_username=None, + donated=[ + ( + row["Lifetime Amount"], + row["Currency"], + row["Last Charge Date"].replace(tzinfo=timezone.utc), + ) + ], + source="patreon", + ) + sponsors.append(sponsor) + + return sponsors + + +if __name__ == "__main__": + now = datetime.now(tz=timezone.utc) + + sponsors = load_github_sponsors_csv( + "data/sponsors/ActivityWatch-sponsorships-all-time.csv" + ) + sponsors += load_opencollective_csv( + "data/sponsors/opencollective-activitywatch-transactions.csv" + ) + sponsors += load_patreon_csv("data/sponsors/patreon-members-866337.csv") + sponsors = sorted(sponsors, key=lambda s: s.total_donated["USD"], reverse=True) + + # filter out sponsors who have donated less than $10 + sponsors = [sponsor for sponsor in sponsors if sponsor.total_donated["USD"] >= 10] + + # print as markdown table + print("| Name | Active? | Total Donated |") + print("| ---- |:-------:| -------------:|") + for sponsor in sponsors: + link = ( + f"([@{sponsor.github_username}](https://github.com/{sponsor.github_username}))" + if sponsor.github_username + else "" + ) + # active if last donation was less than 3 months ago + last_donation: datetime = max(timestamp for _, _, timestamp in sponsor.donated) + is_active = ( + last_donation > now - timedelta(days=90) if sponsor.donated else False + ) + print( + f"| {sponsor.name} {link} | {'✔️' if is_active else ''} | {sponsor.total_donated['USD']:.2f} USD |" + ) diff --git a/output/.empty b/output/.empty new file mode 100644 index 00000000..e69de29b