From 8013f5b3a5d95517fd7234bcf8e1790d20951a2e Mon Sep 17 00:00:00 2001 From: aboydnw Date: Tue, 24 Mar 2026 17:46:22 +0000 Subject: [PATCH 1/7] feat: add ContributorEntry model Co-Authored-By: Claude Opus 4.6 --- python/contributor_network/config.py | 8 ++++++++ python/tests/test_config.py | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/python/contributor_network/config.py b/python/contributor_network/config.py index d8070b6f..9b1c326a 100644 --- a/python/contributor_network/config.py +++ b/python/contributor_network/config.py @@ -7,12 +7,20 @@ from __future__ import annotations +import datetime import tomllib from pathlib import Path from pydantic import BaseModel +class ContributorEntry(BaseModel): + """A contributor with an optional start date for filtering commits.""" + + name: str + start_date: datetime.date | None = None + + class Config(BaseModel): """Configuration for the contributor network visualization. diff --git a/python/tests/test_config.py b/python/tests/test_config.py index 40987527..3aec397f 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -1,7 +1,20 @@ +import datetime from pathlib import Path -from contributor_network.config import Config +from contributor_network.config import Config, ContributorEntry def test_config() -> None: Config.from_toml(Path(__file__).parents[2] / "config.toml") + + +def test_contributor_entry_from_inline_table(): + entry = ContributorEntry(name="Pete Gadomski", start_date=datetime.date(2020, 3, 15)) + assert entry.name == "Pete Gadomski" + assert entry.start_date == datetime.date(2020, 3, 15) + + +def test_contributor_entry_without_start_date(): + entry = ContributorEntry(name="Pete Gadomski") + assert entry.name == "Pete Gadomski" + assert entry.start_date is None From c517f2276818cd55b789ad9f477dafda2b74b2d2 Mon Sep 17 00:00:00 2001 From: aboydnw Date: Tue, 24 Mar 2026 17:47:20 +0000 Subject: [PATCH 2/7] feat: support start_date in contributor config with backwards compat Co-Authored-By: Claude Opus 4.6 --- python/contributor_network/config.py | 47 +++++++++++++++++++++++----- python/tests/test_config.py | 42 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/python/contributor_network/config.py b/python/contributor_network/config.py index 9b1c326a..91ca2208 100644 --- a/python/contributor_network/config.py +++ b/python/contributor_network/config.py @@ -11,7 +11,7 @@ import tomllib from pathlib import Path -from pydantic import BaseModel +from pydantic import BaseModel, field_validator class ContributorEntry(BaseModel): @@ -42,27 +42,60 @@ class Config(BaseModel): organization_name: str organization_nickname: str = "" repositories: list[str] - contributors: dict[ - str, dict[str, str] - ] # Nested: {"devseed": {...}, "alumni": {...}} + contributors: dict[str, dict[str, ContributorEntry]] contributor_padding: int = 40 + @field_validator("contributors", mode="before") + @classmethod + def normalize_contributors(cls, v: dict) -> dict: + """Normalize plain string contributor entries into ContributorEntry dicts.""" + normalized = {} + for category, members in v.items(): + normalized[category] = {} + for login, value in members.items(): + if isinstance(value, str): + normalized[category][login] = {"name": value} + else: + normalized[category][login] = value + return normalized + + def get_contributor_name(self, login: str) -> str: + """Get display name for a contributor by GitHub login.""" + for category in self.contributors.values(): + if login in category: + return category[login].name + raise KeyError(f"Contributor {login!r} not found") + + def get_contributor_start_date(self, login: str) -> datetime.date | None: + """Get start date for a contributor by GitHub login.""" + for category in self.contributors.values(): + if login in category: + return category[login].start_date + raise KeyError(f"Contributor {login!r} not found") + @property def devseed_contributors(self) -> dict[str, str]: """Only Development Seed employees.""" - return self.contributors.get("devseed", {}) + return { + login: entry.name + for login, entry in self.contributors.get("devseed", {}).items() + } @property def alumni_contributors(self) -> dict[str, str]: """Friends and alumni (when enabled).""" - return self.contributors.get("alumni", {}) + return { + login: entry.name + for login, entry in self.contributors.get("alumni", {}).items() + } @property def all_contributors(self) -> dict[str, str]: """All contributors across all categories.""" result = {} for category in self.contributors.values(): - result.update(category) + for login, entry in category.items(): + result[login] = entry.name return result @classmethod diff --git a/python/tests/test_config.py b/python/tests/test_config.py index 3aec397f..76871674 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -18,3 +18,45 @@ def test_contributor_entry_without_start_date(): entry = ContributorEntry(name="Pete Gadomski") assert entry.name == "Pete Gadomski" assert entry.start_date is None + + +def test_config_mixed_contributor_formats(tmp_path): + toml_content = """ +title = "Test" +author = "Test" +description = "Test" +organization_name = "Test" +repositories = [] + +[contributors.devseed] +alice = "Alice Smith" +bob = { name = "Bob Jones", start_date = 2022-06-01 } +""" + config_file = tmp_path / "config.toml" + config_file.write_text(toml_content) + config = Config.from_toml(config_file) + + assert config.get_contributor_name("alice") == "Alice Smith" + assert config.get_contributor_start_date("alice") is None + assert config.get_contributor_name("bob") == "Bob Jones" + assert config.get_contributor_start_date("bob") == datetime.date(2022, 6, 1) + + +def test_config_all_contributors_returns_name_dict(tmp_path): + toml_content = """ +title = "Test" +author = "Test" +description = "Test" +organization_name = "Test" +repositories = [] + +[contributors.devseed] +alice = "Alice Smith" +bob = { name = "Bob Jones", start_date = 2022-06-01 } +""" + config_file = tmp_path / "config.toml" + config_file.write_text(toml_content) + config = Config.from_toml(config_file) + + result = config.all_contributors + assert result == {"alice": "Alice Smith", "bob": "Bob Jones"} From e992153cebd4db3da31ea92ca5bb48069d92f8c3 Mon Sep 17 00:00:00 2001 From: aboydnw Date: Tue, 24 Mar 2026 17:50:31 +0000 Subject: [PATCH 3/7] feat: filter commits by start date across models, client, and CLI Link.from_github() and update_from_github() now accept an optional `since` parameter. When provided, commits are filtered server-side via the GitHub API and counted manually instead of using the unfiltered contributor.contributions count. Co-Authored-By: Claude Opus 4.6 --- python/contributor_network/cli.py | 6 +- python/contributor_network/client.py | 31 +++++-- python/contributor_network/models.py | 64 ++++++++++--- python/tests/test_config.py | 4 +- python/tests/test_models.py | 129 +++++++++++++++++++++++++++ 5 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 python/tests/test_models.py diff --git a/python/contributor_network/cli.py b/python/contributor_network/cli.py index 35486525..7b34be94 100644 --- a/python/contributor_network/cli.py +++ b/python/contributor_network/cli.py @@ -1,3 +1,4 @@ +import datetime import json import subprocess from collections import defaultdict @@ -91,6 +92,9 @@ def fetch( contributors = ( config.all_contributors if all_contributors else config.devseed_contributors ) + start_dates: dict[str, datetime.date | None] = { + login: config.get_contributor_start_date(login) for login in contributors + } print(f"Building data for {len(contributors)} contributors") for repository in repositories: @@ -98,7 +102,7 @@ def fetch( repo = client.get_repo(repository) client.update_repository(repo) print(f"Updating links: {repository}") - client.update_links(repo, contributors) + client.update_links(repo, contributors, start_dates=start_dates) @main.command() diff --git a/python/contributor_network/client.py b/python/contributor_network/client.py index a20aacd0..172bb734 100644 --- a/python/contributor_network/client.py +++ b/python/contributor_network/client.py @@ -1,3 +1,4 @@ +import datetime from pathlib import Path from github import Github @@ -26,12 +27,18 @@ def update_repository(self, repo: Repo) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(repository.model_dump_json()) - def update_links(self, repo: Repo, contributors: dict[str, str]) -> None: + def update_links( + self, + repo: Repo, + contributors: dict[str, str], + start_dates: dict[str, datetime.date | None] | None = None, + ) -> None: """Update the links for a single repository.""" devseed_count = 0 for contributor in repo.get_contributors(): if contributor_name := contributors.get(contributor.login): - self.update_link(repo, contributor, contributor_name) + since = (start_dates or {}).get(contributor.login) + self.update_link(repo, contributor, contributor_name, since=since) devseed_count += 1 # Update repository with community stats (Phase 2) @@ -48,14 +55,26 @@ def update_repository_community_stats( path.write_text(repository.model_dump_json()) def update_link( - self, repo: Repo, contributor: NamedUser, contributor_name: str + self, + repo: Repo, + contributor: NamedUser, + contributor_name: str, + since: datetime.date | None = None, ) -> None: """Update the link for a single contributor to a single repository.""" path = self.directory / "links" / repo.full_name / (contributor.login + ".json") path.parent.mkdir(parents=True, exist_ok=True) if path.exists(): link = Link.model_validate_json(path.read_text()) - link.update_from_github(repo, contributor) + has_commits = link.update_from_github(repo, contributor, since=since) + if not has_commits: + path.unlink() + return + path.write_text(link.model_dump_json()) else: - link = Link.from_github(repo, contributor, contributor_name) - path.write_text(link.model_dump_json()) + new_link = Link.from_github( + repo, contributor, contributor_name, since=since + ) + if new_link is None: + return + path.write_text(new_link.model_dump_json()) diff --git a/python/contributor_network/models.py b/python/contributor_network/models.py index c5ff118a..291c4259 100644 --- a/python/contributor_network/models.py +++ b/python/contributor_network/models.py @@ -18,14 +18,34 @@ class Link(BaseModel): is_recent_contributor: bool = False @classmethod - def from_github(cls, repo: Repo, contributor: NamedUser, author_name: str) -> Link: - commits = repo.get_commits(author=contributor.login) - last_commit = commits[0] - first_commit = commits.reversed[0] + def from_github( + cls, + repo: Repo, + contributor: NamedUser, + author_name: str, + since: datetime.date | None = None, + ) -> Link | None: + if since is not None: + since_dt = datetime.datetime( + since.year, since.month, since.day, tzinfo=datetime.timezone.utc + ) + commits_list = list( + repo.get_commits(author=contributor.login, since=since_dt) + ) + if not commits_list: + return None + commit_count = len(commits_list) + last_commit = commits_list[0] + first_commit = commits_list[-1] + else: + commits = repo.get_commits(author=contributor.login) + last_commit = commits[0] + first_commit = commits.reversed[0] + commit_count = contributor.contributions + commit_sec_min = int(first_commit.commit.author.date.timestamp()) commit_sec_max = int(last_commit.commit.author.date.timestamp()) - # Compute derived fields contribution_span_days = (commit_sec_max - commit_sec_min) // 86400 ninety_days_ago = int( (datetime.datetime.now() - datetime.timedelta(days=90)).timestamp() @@ -35,20 +55,39 @@ def from_github(cls, repo: Repo, contributor: NamedUser, author_name: str) -> Li return cls( author_name=author_name, repo=repo.full_name, - commit_count=contributor.contributions, + commit_count=commit_count, commit_sec_min=commit_sec_min, commit_sec_max=commit_sec_max, contribution_span_days=contribution_span_days, is_recent_contributor=is_recent, ) - def update_from_github(self, repo: Repo, contributor: NamedUser) -> None: - commits = repo.get_commits(author=contributor.login) - last_commit = commits[0] - self.commit_count = contributor.contributions - self.commit_sec_max = int(last_commit.commit.author.date.timestamp()) + def update_from_github( + self, + repo: Repo, + contributor: NamedUser, + since: datetime.date | None = None, + ) -> bool: + if since is not None: + since_dt = datetime.datetime( + since.year, since.month, since.day, tzinfo=datetime.timezone.utc + ) + commits_list = list( + repo.get_commits(author=contributor.login, since=since_dt) + ) + if not commits_list: + return False + self.commit_count = len(commits_list) + last_commit = commits_list[0] + first_commit = commits_list[-1] + self.commit_sec_min = int(first_commit.commit.author.date.timestamp()) + self.commit_sec_max = int(last_commit.commit.author.date.timestamp()) + else: + commits = repo.get_commits(author=contributor.login) + last_commit = commits[0] + self.commit_count = contributor.contributions + self.commit_sec_max = int(last_commit.commit.author.date.timestamp()) - # Recompute derived fields self.contribution_span_days = ( self.commit_sec_max - self.commit_sec_min ) // 86400 @@ -56,6 +95,7 @@ def update_from_github(self, repo: Repo, contributor: NamedUser) -> None: (datetime.datetime.now() - datetime.timedelta(days=90)).timestamp() ) self.is_recent_contributor = self.commit_sec_max > ninety_days_ago + return True class Repository(BaseModel): diff --git a/python/tests/test_config.py b/python/tests/test_config.py index 76871674..b022eb8f 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -9,7 +9,9 @@ def test_config() -> None: def test_contributor_entry_from_inline_table(): - entry = ContributorEntry(name="Pete Gadomski", start_date=datetime.date(2020, 3, 15)) + entry = ContributorEntry( + name="Pete Gadomski", start_date=datetime.date(2020, 3, 15) + ) assert entry.name == "Pete Gadomski" assert entry.start_date == datetime.date(2020, 3, 15) diff --git a/python/tests/test_models.py b/python/tests/test_models.py new file mode 100644 index 00000000..33c81e50 --- /dev/null +++ b/python/tests/test_models.py @@ -0,0 +1,129 @@ +import datetime +from unittest.mock import MagicMock + +from contributor_network.models import Link + + +def _make_commit(timestamp: datetime.datetime) -> MagicMock: + commit = MagicMock() + commit.commit.author.date = timestamp + return commit + + +def _make_contributor(login: str, contributions: int) -> MagicMock: + contributor = MagicMock() + contributor.login = login + contributor.contributions = contributions + return contributor + + +def test_link_from_github_with_since(): + repo = MagicMock() + repo.full_name = "org/repo" + + since_date = datetime.date(2023, 1, 1) + since_dt = datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc) + + commits = [ + _make_commit(datetime.datetime(2023, 6, 15, tzinfo=datetime.timezone.utc)), + _make_commit(datetime.datetime(2023, 3, 1, tzinfo=datetime.timezone.utc)), + ] + paginated = MagicMock() + paginated.__iter__ = MagicMock(return_value=iter(commits)) + repo.get_commits.return_value = paginated + + contributor = _make_contributor("alice", 999) + + link = Link.from_github(repo, contributor, "Alice", since=since_date) + + repo.get_commits.assert_called_once_with(author="alice", since=since_dt) + assert link is not None + assert link.commit_count == 2 + assert link.author_name == "Alice" + + +def test_link_from_github_without_since(): + repo = MagicMock() + repo.full_name = "org/repo" + + commits = [ + _make_commit(datetime.datetime(2023, 6, 15, tzinfo=datetime.timezone.utc)), + _make_commit(datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc)), + ] + paginated = MagicMock() + paginated.__getitem__ = MagicMock(side_effect=lambda i: commits[i]) + paginated.reversed = MagicMock() + paginated.reversed.__getitem__ = MagicMock( + side_effect=lambda i: commits[-(i + 1)] + ) + repo.get_commits.return_value = paginated + + contributor = _make_contributor("alice", 50) + + link = Link.from_github(repo, contributor, "Alice") + + repo.get_commits.assert_called_once_with(author="alice") + assert link is not None + assert link.commit_count == 50 + + +def test_link_from_github_zero_commits_returns_none(): + repo = MagicMock() + repo.full_name = "org/repo" + + paginated = MagicMock() + paginated.__iter__ = MagicMock(return_value=iter([])) + repo.get_commits.return_value = paginated + + contributor = _make_contributor("alice", 10) + + result = Link.from_github( + repo, contributor, "Alice", since=datetime.date(2025, 1, 1) + ) + assert result is None + + +def test_update_from_github_with_since_returns_true(): + link = Link( + author_name="Alice", + repo="org/repo", + commit_count=5, + commit_sec_min=1000000, + commit_sec_max=2000000, + ) + repo = MagicMock() + commits = [ + _make_commit(datetime.datetime(2023, 9, 1, tzinfo=datetime.timezone.utc)), + _make_commit(datetime.datetime(2023, 6, 1, tzinfo=datetime.timezone.utc)), + ] + paginated = MagicMock() + paginated.__iter__ = MagicMock(return_value=iter(commits)) + paginated.__getitem__ = MagicMock(side_effect=lambda i: commits[i]) + repo.get_commits.return_value = paginated + + contributor = _make_contributor("alice", 999) + + result = link.update_from_github(repo, contributor, since=datetime.date(2023, 1, 1)) + assert result is True + assert link.commit_count == 2 + + +def test_update_from_github_with_since_zero_commits_returns_false(): + link = Link( + author_name="Alice", + repo="org/repo", + commit_count=5, + commit_sec_min=1000000, + commit_sec_max=2000000, + ) + repo = MagicMock() + paginated = MagicMock() + paginated.__iter__ = MagicMock(return_value=iter([])) + repo.get_commits.return_value = paginated + + contributor = _make_contributor("alice", 10) + + result = link.update_from_github( + repo, contributor, since=datetime.date(2025, 1, 1) + ) + assert result is False From badfe3a9e946a7cf6d9bdac40c67b61a16cede33 Mon Sep 17 00:00:00 2001 From: aboydnw Date: Tue, 24 Mar 2026 17:55:08 +0000 Subject: [PATCH 4/7] feat: add start dates for all DevSeed contributors Populates start_date for 62 contributors. Also adds firzaariany, renames aliziel to "Alison Ziel" and pantierra to "Felix Delattre". Co-Authored-By: Claude Opus 4.6 --- config.toml | 122 ++++++++++++++++++++++++++-------------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/config.toml b/config.toml index 97f1abb6..ce30be95 100644 --- a/config.toml +++ b/config.toml @@ -91,64 +91,64 @@ repositories = [ # Development Seed employees [contributors.devseed] -AMSCamacho = "Angela Camacho" -AliceR = "Alice Rühl" -LanesGood = "Lane Goodman" -abarciauskas-bgse = "Aimee Barciauskas" -aboydnw = "Anthony Boyd" -aliziel = "Ali Z" -alukach = "Anthony Lukach" -anayeaye = "Alexandra Kirk" -batpad = "Sanjay Bhangar" -benbovy = "Benoît Bovy" -bitner = "David Bitner" -botanical = "Jennifer Tran" -brianna-corremonte = "Brianna Corremonte" -camillecroft = "Camille Croft" -ceholden = "Chris Holden" -chuckwondo = "Chuck Daniels" -ciaransweet = "Ciaran Sweet" -danielfdsilva = "Daniel da Silva" -dannybauman = "Danny Bauman" -dzole0311 = "Gjore Milevski" -emmalu = "Emma Paz" -emmanuelmathot = "Emmanuel Mathot" -faustoperez = "Fausto Pérez" -gadomski = "Pete Gadomski" -geohacker = "Sajjad Anwar" -hanbyul-here = "Hanbyul Jo" -hrodmn = "Henry Rodman" -ianschuler = "Ian Schuler" -ifsimicoded = "Simi Damani" -indraneel = "Indraneel Purohit" -ividito = "Isayah Vidito" -j08lue = "Jonas Sølvsteen" -jjfrench = "Jamison French" -kamicut = "Marc Farra" -kcarini = "Kiri Carini" -kevinbullock = "Kevin Bullock" -kimmurph = "Kim Murphy" -kylebarron = "Kyle Barron" -leothomas = "Leo Thomas" -lillythomas = "Lilly Thomas" -lhoupert = "Loïc Houpert" -maxrjones = "Max Jones" -martham93 = "Martha Morrissey" -olafveerman = "Olaf Veerman" -omniajoe = "Omnia Joehar" -pantierra = "xıʃǝɟ" -ricardoduplos = "Ricardo Duplos" -sandrahoang686 = "Sandra Hoang" -sharkinsspatial = "Sean Harkins" -sharonwanlu = "SharonLu" -smohiudd = "Saadiq Mohiuddin" -srmsoumya = "Soumya Ranjan Mohanty" -sunu = "Tarashish Mishra" -vgeorge = "Vitor George" -vincentsarago = "Vincent Sarago" -weiji14 = "Wei Ji" -wildintellect = "Alex I. Mandel" -willemarcel = "Wille Marcel" -wrynearson = "Will Rynearson" -yellowcap = "Daniel Wiesmann" -zacdezgeo = "Zac Deziel" +AMSCamacho = { name = "Angela Camacho", start_date = 2023-01-09 } +AliceR = { name = "Alice Rühl", start_date = 2020-04-01 } +LanesGood = { name = "Lane Goodman", start_date = 2019-01-07 } +abarciauskas-bgse = { name = "Aimee Barciauskas", start_date = 2018-01-08 } +aboydnw = { name = "Anthony Boyd", start_date = 2021-10-18 } +aliziel = { name = "Alison Ziel", start_date = 2024-05-29 } +alukach = { name = "Anthony Lukach", start_date = 2019-04-18 } +anayeaye = { name = "Alexandra Kirk", start_date = 2021-04-19 } +batpad = { name = "Sanjay Bhangar", start_date = 2018-04-30 } +bitner = { name = "David Bitner", start_date = 2020-01-01 } +botanical = { name = "Jennifer Tran", start_date = 2020-10-05 } +brianna-corremonte = { name = "Brianna Corremonte", start_date = 2025-03-03 } +camillecroft = { name = "Camille Croft", start_date = 2025-09-15 } +ceholden = { name = "Chris Holden", start_date = 2024-07-08 } +chuckwondo = { name = "Chuck Daniels", start_date = 2019-07-08 } +ciaransweet = { name = "Ciaran Sweet", start_date = 2024-07-15 } +danielfdsilva = { name = "Daniel da Silva", start_date = 2015-01-01 } +dannybauman = { name = "Danny Bauman", start_date = 2021-08-09 } +dzole0311 = { name = "Gjore Milevski", start_date = 2024-05-02 } +emmalu = { name = "Emma Paz", start_date = 2023-03-01 } +emmanuelmathot = { name = "Emmanuel Mathot", start_date = 2024-08-01 } +faustoperez = { name = "Fausto Pérez", start_date = 2022-10-03 } +firzaariany = { name = "Firza Ariany", start_date = 2025-11-10 } +gadomski = { name = "Pete Gadomski", start_date = 2024-09-30 } +geohacker = { name = "Sajjad Anwar", start_date = 2018-01-02 } +hanbyul-here = { name = "Hanbyul Jo", start_date = 2021-09-13 } +hrodmn = { name = "Henry Rodman", start_date = 2024-05-22 } +ianschuler = { name = "Ian Schuler", start_date = 2014-01-01 } +ifsimicoded = { name = "Simi Damani", start_date = 2025-10-13 } +indraneel = { name = "Indraneel Purohit", start_date = 2025-03-17 } +ividito = { name = "Isayah Vidito", start_date = 2022-10-11 } +j08lue = { name = "Jonas Sølvsteen", start_date = 2022-08-15 } +jjfrench = { name = "Jamison French", start_date = 2022-06-27 } +kamicut = { name = "Marc Farra", start_date = 2014-09-15 } +kcarini = { name = "Kiri Carini", start_date = 2022-06-06 } +kevinbullock = { name = "Kevin Bullock", start_date = 2024-02-05 } +kimmurph = { name = "Kim Murphy", start_date = 2018-02-08 } +kylebarron = { name = "Kyle Barron", start_date = 2023-08-14 } +leothomas = { name = "Leo Thomas", start_date = 2020-05-26 } +lhoupert = { name = "Loïc Houpert", start_date = 2025-11-17 } +lillythomas = { name = "Lilly Thomas", start_date = 2020-04-01 } +martham93 = { name = "Martha Morrissey", start_date = 2025-09-22 } +maxrjones = { name = "Max Jones", start_date = 2024-07-15 } +olafveerman = { name = "Olaf Veerman", start_date = 2015-01-01 } +omniajoe = { name = "Omnia Joehar", start_date = 2020-05-26 } +pantierra = { name = "Felix Delattre", start_date = 2024-10-07 } +ricardoduplos = { name = "Ricardo Duplos", start_date = 2015-01-01 } +sandrahoang686 = { name = "Sandra Hoang", start_date = 2023-06-28 } +sharkinsspatial = { name = "Sean Harkins", start_date = 2018-11-15 } +sharonwanlu = { name = "SharonLu", start_date = 2025-05-12 } +smohiudd = { name = "Saadiq Mohiuddin", start_date = 2022-11-28 } +srmsoumya = { name = "Soumya Ranjan Mohanty", start_date = 2021-12-06 } +sunu = { name = "Tarashish Mishra", start_date = 2022-10-31 } +vgeorge = { name = "Vitor George", start_date = 2018-08-01 } +vincentsarago = { name = "Vincent Sarago", start_date = 2019-01-07 } +weiji14 = { name = "Wei Ji", start_date = 2023-02-01 } +wildintellect = { name = "Alex I. Mandel", start_date = 2019-11-01 } +willemarcel = { name = "Wille Marcel", start_date = 2021-07-26 } +wrynearson = { name = "Will Rynearson", start_date = 2022-06-13 } +yellowcap = { name = "Daniel Wiesmann", start_date = 2023-03-06 } +zacdezgeo = { name = "Zac Deziel", start_date = 2023-01-30 } \ No newline at end of file From 262c608521e2d03238a337629411b9dd01f34b50 Mon Sep 17 00:00:00 2001 From: aboydnw Date: Tue, 24 Mar 2026 17:55:12 +0000 Subject: [PATCH 5/7] docs: document contributor start_date config format Co-Authored-By: Claude Opus 4.6 --- .claude/CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a44daca4..75ebf80c 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -262,8 +262,12 @@ Configured in `src/config/theme.ts`. ### Add a New Contributor 1. Edit `config.toml` - add to `[contributors.devseed]` or `[contributors.alumni]` + - With start date: `username = { name = "Full Name", start_date = 2024-01-15 }` + - Without start date (legacy): `username = "Full Name"` — all commits are counted 2. Re-run data fetch and build (above) +When a `start_date` is set, only commits after that date are counted as DevSeed contributions. This prevents pre-employment open-source work from being attributed to the organization. + ### Making Frontend Changes 1. Run `npm run dev` for the Vite dev server with HMR From 55bb21f6eaf8902a816f353efe34455f2f536081 Mon Sep 17 00:00:00 2001 From: aboydnw Date: Tue, 24 Mar 2026 21:16:42 +0000 Subject: [PATCH 6/7] style: fix ruff formatting in test_models.py Co-Authored-By: Claude Opus 4.6 --- python/tests/test_models.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/python/tests/test_models.py b/python/tests/test_models.py index 33c81e50..54274dd9 100644 --- a/python/tests/test_models.py +++ b/python/tests/test_models.py @@ -53,9 +53,7 @@ def test_link_from_github_without_since(): paginated = MagicMock() paginated.__getitem__ = MagicMock(side_effect=lambda i: commits[i]) paginated.reversed = MagicMock() - paginated.reversed.__getitem__ = MagicMock( - side_effect=lambda i: commits[-(i + 1)] - ) + paginated.reversed.__getitem__ = MagicMock(side_effect=lambda i: commits[-(i + 1)]) repo.get_commits.return_value = paginated contributor = _make_contributor("alice", 50) @@ -123,7 +121,5 @@ def test_update_from_github_with_since_zero_commits_returns_false(): contributor = _make_contributor("alice", 10) - result = link.update_from_github( - repo, contributor, since=datetime.date(2025, 1, 1) - ) + result = link.update_from_github(repo, contributor, since=datetime.date(2025, 1, 1)) assert result is False From d00099ebfcafdf10ec2bc78ea9a493aa9cd29ccb Mon Sep 17 00:00:00 2001 From: aboydnw Date: Tue, 24 Mar 2026 21:19:33 +0000 Subject: [PATCH 7/7] fix: add mypy type annotation for normalized dict Co-Authored-By: Claude Opus 4.6 --- python/contributor_network/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/contributor_network/config.py b/python/contributor_network/config.py index 91ca2208..2748223a 100644 --- a/python/contributor_network/config.py +++ b/python/contributor_network/config.py @@ -49,7 +49,7 @@ class Config(BaseModel): @classmethod def normalize_contributors(cls, v: dict) -> dict: """Normalize plain string contributor entries into ContributorEntry dicts.""" - normalized = {} + normalized: dict[str, dict[str, dict[str, object]]] = {} for category, members in v.items(): normalized[category] = {} for login, value in members.items():