|
| 1 | +""" |
| 2 | +Analyze sponsor data to generate a sponsors table on the website |
| 3 | +""" |
| 4 | + |
| 5 | +from dataclasses import dataclass, field |
| 6 | +from datetime import datetime, timedelta, timezone |
| 7 | +from typing import Optional |
| 8 | + |
| 9 | +import pandas as pd |
| 10 | + |
| 11 | +Amount = float |
| 12 | +Currency = str |
| 13 | + |
| 14 | + |
| 15 | +@dataclass |
| 16 | +class Sponsor: |
| 17 | + name: str |
| 18 | + github_username: Optional[str] = None |
| 19 | + donated: list[tuple[Amount, Currency, datetime]] = field(default_factory=list) |
| 20 | + source: str = "unknown" |
| 21 | + |
| 22 | + def __repr__(self): |
| 23 | + return f"<Sponsor {self.name} ({self.github_username}) on {self.source} total_donated={self.total_donated}>" |
| 24 | + |
| 25 | + @property |
| 26 | + def total_donated(self): |
| 27 | + # group donated by currency, aggregate by sum |
| 28 | + currencies = set([currency for _, currency, _ in self.donated]) |
| 29 | + total_donated = {} |
| 30 | + for currency in currencies: |
| 31 | + total_donated[currency] = sum( |
| 32 | + [amount for amount, c, _ in self.donated if c == currency] |
| 33 | + ) |
| 34 | + return total_donated |
| 35 | + |
| 36 | + |
| 37 | +def load_github_sponsors_csv(filename: str) -> list[Sponsor]: |
| 38 | + """The CSV looks like the following: |
| 39 | +
|
| 40 | + 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 |
| 41 | + 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, |
| 42 | + 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, |
| 43 | + 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, |
| 44 | + """ |
| 45 | + |
| 46 | + df = pd.read_csv( |
| 47 | + filename, parse_dates=["Sponsorship Started On", "Transaction Date"] |
| 48 | + ) |
| 49 | + df = df[df["Is Public?"] == True] # noqa: E712 |
| 50 | + df = df[df["Status"] == "settled"] |
| 51 | + |
| 52 | + sponsors = [] |
| 53 | + handles = df["Sponsor Handle"].unique() |
| 54 | + for handle in handles: |
| 55 | + sponsor = Sponsor( |
| 56 | + name="placeholder", |
| 57 | + github_username=handle, |
| 58 | + donated=[], |
| 59 | + source="github", |
| 60 | + ) |
| 61 | + for _, row in df.iterrows(): |
| 62 | + if row["Sponsor Handle"] != handle: |
| 63 | + continue |
| 64 | + sponsor.name = row["Sponsor Profile Name"] |
| 65 | + sponsor.donated.append( |
| 66 | + ( |
| 67 | + float(row["Processed Amount"].replace("$", "")), |
| 68 | + "USD", |
| 69 | + row["Transaction Date"], |
| 70 | + ) |
| 71 | + ) |
| 72 | + |
| 73 | + sponsors.append(sponsor) |
| 74 | + |
| 75 | + return sponsors |
| 76 | + |
| 77 | + |
| 78 | +def load_opencollective_csv(filename: str) -> list[Sponsor]: |
| 79 | + """The CSV looks like this: |
| 80 | +
|
| 81 | + "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" |
| 82 | + "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",,"",,, |
| 83 | + "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",,,,"",,, |
| 84 | + "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",,"",,, |
| 85 | + """ |
| 86 | + |
| 87 | + df = pd.read_csv(filename, parse_dates=["datetime"]) |
| 88 | + df = df[df["type"] == "CREDIT"] |
| 89 | + |
| 90 | + sponsors = [] |
| 91 | + handles = df["oppositeAccountSlug"].unique() |
| 92 | + for handle in handles: |
| 93 | + sponsor = Sponsor( |
| 94 | + name="placeholder", |
| 95 | + github_username=handle, |
| 96 | + donated=[], |
| 97 | + source="opencollective", |
| 98 | + ) |
| 99 | + for _, row in df.iterrows(): |
| 100 | + if row["oppositeAccountSlug"] != handle: |
| 101 | + continue |
| 102 | + sponsor.name = row["oppositeAccountName"] |
| 103 | + sponsor.donated.append( |
| 104 | + ( |
| 105 | + row["amount"], |
| 106 | + row["currency"], |
| 107 | + row["datetime"].replace(tzinfo=timezone.utc), |
| 108 | + ) |
| 109 | + ) |
| 110 | + |
| 111 | + sponsors.append(sponsor) |
| 112 | + |
| 113 | + # remove 'GitHub Sponsors' from sponsors |
| 114 | + sponsors = [sponsor for sponsor in sponsors if sponsor.name != "GitHub Sponsors"] |
| 115 | + |
| 116 | + # subtract $3002 from Kerkko (actually FUUG.fi) |
| 117 | + for sponsor in sponsors: |
| 118 | + if sponsor.name == "Kerkko Pelttari": |
| 119 | + sponsor.donated.append((-3002, "USD", datetime.now(tz=timezone.utc))) |
| 120 | + |
| 121 | + return sponsors |
| 122 | + |
| 123 | + |
| 124 | +def load_patreon_csv(filename: str) -> list[Sponsor]: |
| 125 | + """The CSV looks like: |
| 126 | +
|
| 127 | + 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 |
| 128 | + Karan singh,[email protected],,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 |
| 129 | + Dan Thompson,[email protected],,,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 |
| 130 | + """ |
| 131 | + |
| 132 | + df = pd.read_csv(filename, parse_dates=["Patronage Since Date", "Last Charge Date"]) |
| 133 | + |
| 134 | + sponsors = [] |
| 135 | + for _, row in df.iterrows(): |
| 136 | + sponsor = Sponsor( |
| 137 | + name=row["Name"], |
| 138 | + github_username=None, |
| 139 | + donated=[ |
| 140 | + ( |
| 141 | + row["Lifetime Amount"], |
| 142 | + row["Currency"], |
| 143 | + row["Last Charge Date"].replace(tzinfo=timezone.utc), |
| 144 | + ) |
| 145 | + ], |
| 146 | + source="patreon", |
| 147 | + ) |
| 148 | + sponsors.append(sponsor) |
| 149 | + |
| 150 | + return sponsors |
| 151 | + |
| 152 | + |
| 153 | +if __name__ == "__main__": |
| 154 | + now = datetime.now(tz=timezone.utc) |
| 155 | + |
| 156 | + sponsors = load_github_sponsors_csv( |
| 157 | + "data/sponsors/ActivityWatch-sponsorships-all-time.csv" |
| 158 | + ) |
| 159 | + sponsors += load_opencollective_csv( |
| 160 | + "data/sponsors/opencollective-activitywatch-transactions.csv" |
| 161 | + ) |
| 162 | + sponsors += load_patreon_csv("data/sponsors/patreon-members-866337.csv") |
| 163 | + sponsors = sorted(sponsors, key=lambda s: s.total_donated["USD"], reverse=True) |
| 164 | + |
| 165 | + # filter out sponsors who have donated less than $10 |
| 166 | + sponsors = [sponsor for sponsor in sponsors if sponsor.total_donated["USD"] >= 10] |
| 167 | + |
| 168 | + # print as markdown table |
| 169 | + print("| Name | Active? | Total Donated |") |
| 170 | + print("| ---- |:-------:| -------------:|") |
| 171 | + for sponsor in sponsors: |
| 172 | + link = ( |
| 173 | + f"([@{sponsor.github_username}](https://github.com/{sponsor.github_username}))" |
| 174 | + if sponsor.github_username |
| 175 | + else "" |
| 176 | + ) |
| 177 | + # active if last donation was less than 3 months ago |
| 178 | + last_donation: datetime = max(timestamp for _, _, timestamp in sponsor.donated) |
| 179 | + is_active = ( |
| 180 | + last_donation > now - timedelta(days=90) if sponsor.donated else False |
| 181 | + ) |
| 182 | + print( |
| 183 | + f"| {sponsor.name} {link} | {'✔️' if is_active else ''} | {sponsor.total_donated['USD']:.2f} USD |" |
| 184 | + ) |
0 commit comments