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 <noreply@anthropic.com>
This commit is contained in:
52
src/gitea_dashboard/collector.py
Normal file
52
src/gitea_dashboard/collector.py
Normal file
@@ -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
|
||||||
130
tests/test_collector.py
Normal file
130
tests/test_collector.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user