From b52bc72ce88832ec007cfc95dcbfdb66130935c7 Mon Sep 17 00:00:00 2001 From: sylvain Date: Tue, 10 Mar 2026 18:51:17 +0100 Subject: [PATCH] feat(collector): add RepoData dataclass and collect_all (fixes #2) - RepoData dataclass with all repo fields - collect_all enriches each repo with release and milestones - Computes open_issues = open_issues_count - open_pr_counter - 6 unit tests with mocked GiteaClient Co-Authored-By: Claude Opus 4.6 --- src/gitea_dashboard/collector.py | 52 +++++++++++++ tests/test_collector.py | 130 +++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 src/gitea_dashboard/collector.py create mode 100644 tests/test_collector.py diff --git a/src/gitea_dashboard/collector.py b/src/gitea_dashboard/collector.py new file mode 100644 index 0000000..c194f15 --- /dev/null +++ b/src/gitea_dashboard/collector.py @@ -0,0 +1,52 @@ +"""Collecte et agregation des donnees repos Gitea.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from gitea_dashboard.client import GiteaClient + + +@dataclass +class RepoData: + """Donnees agregees d'un repo.""" + + name: str + full_name: str + description: str + open_issues: int # open_issues_count - open_pr_counter + is_fork: bool + is_archived: bool + is_mirror: bool + latest_release: dict | None # {tag_name, published_at} ou None + milestones: list[dict] # [{title, open_issues, closed_issues, due_on}] + + +def collect_all(client: GiteaClient) -> list[RepoData]: + """Collecte les donnees de tous les repos. + + Pour chaque repo : enrichit avec release et milestones. + Calcule open_issues = open_issues_count - open_pr_counter. + """ + repos = client.get_repos() + result: list[RepoData] = [] + + for repo in repos: + owner = repo["owner"]["login"] + name = repo["name"] + + result.append( + RepoData( + name=name, + full_name=repo["full_name"], + description=repo.get("description", "") or "", + open_issues=repo["open_issues_count"] - repo["open_pr_counter"], + is_fork=repo["fork"], + is_archived=repo["archived"], + is_mirror=repo["mirror"], + latest_release=client.get_latest_release(owner, name), + milestones=client.get_milestones(owner, name), + ) + ) + + return result diff --git a/tests/test_collector.py b/tests/test_collector.py new file mode 100644 index 0000000..d9870c3 --- /dev/null +++ b/tests/test_collector.py @@ -0,0 +1,130 @@ +"""Tests for data collector.""" + +from unittest.mock import MagicMock + +from gitea_dashboard.collector import RepoData, collect_all + + +def _make_repo( + name="my-repo", + full_name="admin/my-repo", + description="A repo", + open_issues_count=5, + open_pr_counter=2, + fork=False, + archived=False, + mirror=False, + owner_login="admin", +): + """Build a fake repo dict as returned by the Gitea API.""" + return { + "name": name, + "full_name": full_name, + "description": description, + "open_issues_count": open_issues_count, + "open_pr_counter": open_pr_counter, + "fork": fork, + "archived": archived, + "mirror": mirror, + "owner": {"login": owner_login}, + } + + +class TestCollectAll: + """Test collect_all function.""" + + def test_basic_repo(self): + """Collects repo data with release and milestones.""" + client = MagicMock() + client.get_repos.return_value = [_make_repo()] + client.get_latest_release.return_value = { + "tag_name": "v1.0", + "published_at": "2026-01-01", + } + client.get_milestones.return_value = [ + {"title": "v2.0", "open_issues": 3, "closed_issues": 2, "due_on": None}, + ] + + result = collect_all(client) + + assert len(result) == 1 + repo = result[0] + assert isinstance(repo, RepoData) + assert repo.name == "my-repo" + assert repo.full_name == "admin/my-repo" + assert repo.description == "A repo" + assert repo.open_issues == 3 # 5 - 2 + assert repo.is_fork is False + assert repo.is_archived is False + assert repo.is_mirror is False + assert repo.latest_release == {"tag_name": "v1.0", "published_at": "2026-01-01"} + assert len(repo.milestones) == 1 + + def test_repo_without_release(self): + """Repo with no release gets None for latest_release.""" + client = MagicMock() + client.get_repos.return_value = [_make_repo()] + client.get_latest_release.return_value = None + client.get_milestones.return_value = [] + + result = collect_all(client) + + assert result[0].latest_release is None + + def test_repo_without_milestones(self): + """Repo with no milestones gets empty list.""" + client = MagicMock() + client.get_repos.return_value = [_make_repo()] + client.get_latest_release.return_value = None + client.get_milestones.return_value = [] + + result = collect_all(client) + + assert result[0].milestones == [] + + def test_open_issues_subtracts_prs(self): + """open_issues = open_issues_count - open_pr_counter.""" + client = MagicMock() + client.get_repos.return_value = [ + _make_repo(open_issues_count=10, open_pr_counter=4), + ] + client.get_latest_release.return_value = None + client.get_milestones.return_value = [] + + result = collect_all(client) + + assert result[0].open_issues == 6 + + def test_multiple_repos(self): + """Collects data for multiple repos.""" + client = MagicMock() + client.get_repos.return_value = [ + _make_repo(name="repo-a", full_name="admin/repo-a"), + _make_repo(name="repo-b", full_name="org/repo-b", owner_login="org"), + ] + client.get_latest_release.return_value = None + client.get_milestones.return_value = [] + + result = collect_all(client) + + assert len(result) == 2 + assert result[0].name == "repo-a" + assert result[1].name == "repo-b" + # Verify correct owner/repo passed to enrichment calls + client.get_latest_release.assert_any_call("admin", "repo-a") + client.get_latest_release.assert_any_call("org", "repo-b") + + def test_fork_and_archived_flags(self): + """Fork and archived flags are propagated.""" + client = MagicMock() + client.get_repos.return_value = [ + _make_repo(fork=True, archived=True, mirror=True), + ] + client.get_latest_release.return_value = None + client.get_milestones.return_value = [] + + result = collect_all(client) + + assert result[0].is_fork is True + assert result[0].is_archived is True + assert result[0].is_mirror is True