feat: port remaining cohort scripts and make reusable

- Port 19 cohort scripts from /home/naomi/docs/cohort/
- Replace all hardcoded tokens and dotenv usage with os.environ
- Add pandas==3.0.1 dependency
- Add E501 to ruff ignore list for Discord message string content
- Make remove_resigned_members.py reusable (empty RESIGNED_IDS constant)
- Make update_roster_messages.py reusable (iterates all teams from JSON)
- Exclude 12 one-off/event-specific scripts as non-reusable
This commit is contained in:
2026-02-23 15:23:10 -08:00
parent e481823e06
commit 4fdb5d06f1
20 changed files with 2108 additions and 1 deletions
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""Remove a member from the Spring 2026 Cohort.
This script:
1. Removes from team_assignments.json (so activity checker stops tracking them)
2. Removes from discord_to_github.json
3. Removes from GitHub teams
4. Removes Discord roles
5. Sends a message to the team channel announcing the removal
6. Outputs notes to add to COHORT_NOTES.md
Usage:
python remove_member.py <discord_id>
"""
import asyncio
import json
import os
import sys
from datetime import datetime, timezone
import aiohttp
DISCORD_BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
DISCORD_API_BASE = "https://discord.com/api/v10"
DISCORD_GUILD_ID = "692816967895220344"
COHORT_ROLE_ID = "1464316447591985194"
TEXT_CHANNEL_IDS = {
"Jade Jasmine": "1464316501573107886",
"Crimson Dahlia": "1464316744909852682",
"Rose Camellia": "1464316751268286611",
"Amber Wisteria": "1464316761410113641",
"Ivory Orchid": "1464316770889240730",
"Teal Iris": "1464316776459407448",
"Peach Gardenia": "1464316785040953543",
"Violet Carnation": "1464316805261824032",
"Azure Lotus": "1464316814455472139",
"Coral Sunflower": "1464316819711066263",
"Indigo Tulip": "1464316826384072925",
"Scarlet Hydrangea": "1464316839306985506",
"Mint Narcissus": "1464316844251807952",
"Sage Marigold": "1464316850669093040",
}
class MemberRemover:
"""Handles the complete removal of a cohort member."""
def __init__(self, discord_id: str) -> None:
self.discord_id = discord_id
self.headers = {
"Authorization": f"Bot {DISCORD_BOT_TOKEN}",
"Content-Type": "application/json",
}
self.session: aiohttp.ClientSession | None = None
self.github_username: str | None = None
self.team_name: str | None = None
self.role: str | None = None
self.team_role_id: str | None = None
async def __aenter__(self) -> "MemberRemover":
self.session = aiohttp.ClientSession()
return self
async def __aexit__(
self, exc_type: object, exc_val: object, exc_tb: object
) -> None:
if self.session:
await self.session.close()
def find_member_info(self) -> bool:
"""Find which team the member is on and their role."""
with open("team_assignments.json") as f:
teams = json.load(f)
for team in teams:
if self.discord_id in team["leaders"]:
self.team_name = team["name"]
self.role = "leader"
return True
if self.discord_id in team["participants"]:
self.team_name = team["name"]
self.role = "participant"
return True
return False
def remove_from_team_assignments(self) -> None:
"""Remove member from team_assignments.json."""
with open("team_assignments.json") as f:
teams = json.load(f)
for team in teams:
if self.discord_id in team["leaders"]:
team["leaders"].remove(self.discord_id)
print(
f"✅ Removed from {team['name']} leaders in team_assignments.json"
)
elif self.discord_id in team["participants"]:
team["participants"].remove(self.discord_id)
print(
f"✅ Removed from {team['name']} participants in team_assignments.json"
)
with open("team_assignments.json", "w") as f:
json.dump(teams, f, indent=2)
def remove_from_discord_to_github(self) -> None:
"""Remove member from discord_to_github.json."""
with open("discord_to_github.json") as f:
mappings = json.load(f)
if self.discord_id in mappings:
self.github_username = mappings[self.discord_id]
del mappings[self.discord_id]
with open("discord_to_github.json", "w") as f:
json.dump(mappings, f, indent=2)
print(f"✅ Removed {self.github_username} from discord_to_github.json")
else:
print("⚠️ No GitHub username found in discord_to_github.json")
async def remove_from_github_teams(self) -> None:
"""Print instructions to remove member from GitHub organization teams."""
if not self.github_username:
print("⚠️ Skipping GitHub removal (no username)")
return
print(f"️ Please manually remove {self.github_username} from GitHub teams:")
print(f" - Team: {self.team_name}")
if self.role == "leader":
print(f" - Team: {self.team_name} Leaders")
async def remove_discord_roles(self) -> None:
"""Remove Discord roles from the member."""
with open("team_message_ids.json") as f:
team_message_data = json.load(f)
if self.team_name not in team_message_data:
print(f"⚠️ Could not find role ID for team {self.team_name}")
return
self.team_role_id = team_message_data[self.team_name]["role_id"]
url = (
f"{DISCORD_API_BASE}/guilds/{DISCORD_GUILD_ID}"
f"/members/{self.discord_id}/roles/{self.team_role_id}"
)
async with self.session.delete(url, headers=self.headers) as response:
if response.status == 204:
print(f"✅ Removed {self.team_name} Discord role")
else:
print(f"❌ Failed to remove team role: {response.status}")
url = (
f"{DISCORD_API_BASE}/guilds/{DISCORD_GUILD_ID}"
f"/members/{self.discord_id}/roles/{COHORT_ROLE_ID}"
)
async with self.session.delete(url, headers=self.headers) as response:
if response.status == 204:
print("✅ Removed Spring 2026 Cohort Discord role")
else:
print(f"❌ Failed to remove cohort role: {response.status}")
async def send_team_message(self) -> None:
"""Send a message to the team channel announcing the removal."""
channel_id = TEXT_CHANNEL_IDS[self.team_name]
message = (
f"<@{self.discord_id}> has been removed from the cohort for inactivity."
)
url = f"{DISCORD_API_BASE}/channels/{channel_id}/messages"
data = {"content": message}
async with self.session.post(url, headers=self.headers, json=data) as response:
if response.status == 200:
print(f"✅ Sent removal message to {self.team_name} channel")
else:
print(f"❌ Failed to send message: {response.status}")
def generate_cohort_notes(self) -> str:
"""Generate text to add to COHORT_NOTES.md."""
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
notes = f"\n## {today} - Member Removal\n\n"
notes += "| Discord ID | GitHub Username | Team | Role |\n"
notes += "|------------|-----------------|------|------|\n"
notes += (
f"| {self.discord_id} | {self.github_username or '-'} "
f"| {self.team_name} | {self.role.capitalize()} |\n"
)
return notes
async def main() -> None:
"""Remove a member from the cohort."""
if len(sys.argv) != 2:
print("Usage: python remove_member.py <discord_id>")
sys.exit(1)
discord_id = sys.argv[1]
async with MemberRemover(discord_id) as remover:
if not remover.find_member_info():
print(f"❌ Discord ID {discord_id} not found in any team")
sys.exit(1)
print("\n📋 Found member:")
print(f" Discord ID: {discord_id}")
print(f" Team: {remover.team_name}")
print(f" Role: {remover.role}")
print()
confirm = input("Proceed with removal? (yes/no): ")
if confirm.lower() != "yes":
print("❌ Removal cancelled")
sys.exit(0)
print("\n🚀 Starting removal process...\n")
remover.remove_from_team_assignments()
remover.remove_from_discord_to_github()
await remover.remove_from_github_teams()
await remover.remove_discord_roles()
await remover.send_team_message()
print("\n" + "=" * 80)
print("📝 Add this to COHORT_NOTES.md:")
print("=" * 80)
print(remover.generate_cohort_notes())
print("=" * 80)
print("\n✅ Member removal complete!")
if __name__ == "__main__":
asyncio.run(main())