|
| 1 | +""" |
| 2 | +Script to add a new BPD Council member. |
| 3 | +
|
| 4 | +Steps performed: |
| 5 | +1. Adds the member's name to the Council list in _data/leadership.json |
| 6 | +2. Copies/converts the provided image to assets/images/ |
| 7 | +3. Creates a blog post announcement in _posts/ |
| 8 | +
|
| 9 | +Usage: |
| 10 | + python scripts/add_council_member.py \ |
| 11 | + --name "First Last" \ |
| 12 | + --image /path/to/photo.webp \ |
| 13 | + --bio "Their bio text here..." \ |
| 14 | + [--linkedin "https://linkedin.com/in/..."] \ |
| 15 | + [--date 2026-03-22] |
| 16 | +""" |
| 17 | + |
| 18 | +import argparse |
| 19 | +import json |
| 20 | +import shutil |
| 21 | +import subprocess |
| 22 | +import re |
| 23 | +from datetime import date |
| 24 | +from pathlib import Path |
| 25 | + |
| 26 | +ROOT = Path(__file__).resolve().parent.parent |
| 27 | +LEADERSHIP_JSON = ROOT / "_data" / "leadership.json" |
| 28 | +POSTS_DIR = ROOT / "_posts" |
| 29 | +IMAGES_DIR = ROOT / "assets" / "images" |
| 30 | + |
| 31 | +COUNCIL_BLURB = ( |
| 32 | + "The BPD council is the sounding board for decisions that need to be made " |
| 33 | + "in Black Python Devs. They are the advisory group who share how our actions " |
| 34 | + "can impact their communities. The council is made up of organizers, BPD leaders " |
| 35 | + "and those in leadership positions around the Python world." |
| 36 | +) |
| 37 | + |
| 38 | + |
| 39 | +def slugify(name: str) -> str: |
| 40 | + return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") |
| 41 | + |
| 42 | + |
| 43 | +def copy_image(source: Path, name_slug: str) -> str: |
| 44 | + """Copy the image to assets/images as webp and return the site-relative path.""" |
| 45 | + dest_name = f"{name_slug}-council.webp" |
| 46 | + dest = IMAGES_DIR / dest_name |
| 47 | + |
| 48 | + if source.suffix.lower() == ".webp": |
| 49 | + shutil.copy2(source, dest) |
| 50 | + else: |
| 51 | + subprocess.run( |
| 52 | + ["magick", str(source), str(dest)], |
| 53 | + check=True, |
| 54 | + ) |
| 55 | + |
| 56 | + return f"/assets/images/{dest_name}" |
| 57 | + |
| 58 | + |
| 59 | +def update_leadership(name: str) -> None: |
| 60 | + """Add the name to the Council list in leadership.json.""" |
| 61 | + data = json.loads(LEADERSHIP_JSON.read_text()) |
| 62 | + |
| 63 | + if name in data["Council"]: |
| 64 | + print(f"⚠ {name} is already in the Council list, skipping update.") |
| 65 | + return |
| 66 | + |
| 67 | + data["Council"].append(name) |
| 68 | + LEADERSHIP_JSON.write_text(json.dumps(data, indent=2) + "\n") |
| 69 | + print(f"✓ Added {name} to leadership.json Council list.") |
| 70 | + |
| 71 | + |
| 72 | +def create_blog_post( |
| 73 | + name: str, |
| 74 | + bio: str, |
| 75 | + image_path: str, |
| 76 | + linkedin: str | None = None, |
| 77 | + post_date: date | None = None, |
| 78 | +) -> Path: |
| 79 | + """Create the announcement blog post.""" |
| 80 | + post_date = post_date or date.today() |
| 81 | + name_slug = slugify(name) |
| 82 | + filename = f"{post_date.isoformat()}-{name_slug}-added-to-bpd-council.md" |
| 83 | + filepath = POSTS_DIR / filename |
| 84 | + |
| 85 | + # Build the name link |
| 86 | + if linkedin: |
| 87 | + name_link = f"[{name}]({linkedin})" |
| 88 | + else: |
| 89 | + name_link = name |
| 90 | + |
| 91 | + first_name = name.split()[0] |
| 92 | + |
| 93 | + content = f"""--- |
| 94 | +author: |
| 95 | + - Jay Miller |
| 96 | +date: {post_date.isoformat()} |
| 97 | +description: The BPD Council has recognized the work of {name} and invited |
| 98 | + them to join the Council. |
| 99 | +featured_image: {image_path} |
| 100 | +title: {name} Added to BPD Council |
| 101 | +--- |
| 102 | +
|
| 103 | +Congratulations to {name_link} for recognition and consideration for the BPD Council! |
| 104 | +
|
| 105 | +{bio} |
| 106 | +
|
| 107 | +{COUNCIL_BLURB} |
| 108 | +
|
| 109 | +Congratulations again to {first_name}! |
| 110 | +""" |
| 111 | + |
| 112 | + filepath.write_text(content) |
| 113 | + print(f"✓ Created blog post: {filepath.relative_to(ROOT)}") |
| 114 | + return filepath |
| 115 | + |
| 116 | + |
| 117 | +def main(): |
| 118 | + parser = argparse.ArgumentParser( |
| 119 | + description="Add a new member to the BPD Council" |
| 120 | + ) |
| 121 | + parser.add_argument("--name", required=True, help="Full name of the new council member") |
| 122 | + parser.add_argument("--image", required=True, help="Path to the member's photo") |
| 123 | + parser.add_argument("--bio", required=True, help="Bio paragraph(s) for the blog post") |
| 124 | + parser.add_argument("--linkedin", default=None, help="LinkedIn profile URL (optional)") |
| 125 | + parser.add_argument( |
| 126 | + "--date", |
| 127 | + default=None, |
| 128 | + help="Post date in YYYY-MM-DD format (defaults to today)", |
| 129 | + ) |
| 130 | + args = parser.parse_args() |
| 131 | + |
| 132 | + post_date = date.fromisoformat(args.date) if args.date else date.today() |
| 133 | + image_source = Path(args.image).expanduser().resolve() |
| 134 | + |
| 135 | + if not image_source.exists(): |
| 136 | + parser.error(f"Image file not found: {image_source}") |
| 137 | + |
| 138 | + name_slug = slugify(args.name) |
| 139 | + |
| 140 | + # 1. Copy image |
| 141 | + image_path = copy_image(image_source, name_slug) |
| 142 | + print(f"✓ Image saved to {image_path}") |
| 143 | + |
| 144 | + # 2. Update leadership.json |
| 145 | + update_leadership(args.name) |
| 146 | + |
| 147 | + # 3. Create blog post |
| 148 | + create_blog_post( |
| 149 | + name=args.name, |
| 150 | + bio=args.bio, |
| 151 | + image_path=image_path, |
| 152 | + linkedin=args.linkedin, |
| 153 | + post_date=post_date, |
| 154 | + ) |
| 155 | + |
| 156 | + print("\nDone! Review the changes and commit when ready.") |
| 157 | + |
| 158 | + |
| 159 | +if __name__ == "__main__": |
| 160 | + main() |
0 commit comments