feat(cli): add --repo and --exclude filtering (fixes #5)

Add argparse-based CLI parsing with repeatable --repo/-r (include) and
--exclude/-x (exclude) options. Filtering is case-insensitive substring
matching, applied post-fetch in collect_all() per ADR-005.

- parse_args() separated from main() for testability
- main(argv=None) accepts argv for test injection
- collect_all() gains optional include/exclude parameters
- 14 new tests (8 filtering + 6 CLI parsing/integration)
- All 51 tests pass, backward compatible (no args = v1.0.0 behavior)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sylvain
2026-03-11 04:35:42 +01:00
parent 0f8e34edf3
commit 2232260821
5 changed files with 231 additions and 19 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import argparse
import os
import sys
@@ -15,15 +16,50 @@ from gitea_dashboard.display import render_dashboard
_DEFAULT_URL = "http://192.168.0.106:3000"
def main() -> None:
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse les arguments CLI.
Options:
--repo / -r : noms de repos a inclure (repeatable)
--exclude / -x : noms de repos a exclure (repeatable)
Returns:
Namespace avec .repo (list[str] | None) et .exclude (list[str] | None)
"""
parser = argparse.ArgumentParser(
description="Dashboard CLI affichant l'etat des repos Gitea.",
)
parser.add_argument(
"--repo",
"-r",
action="append",
default=None,
help="Filtrer par nom de repo (sous-chaine, insensible a la casse). Repeatable.",
)
parser.add_argument(
"--exclude",
"-x",
action="append",
default=None,
help="Exclure les repos par nom (sous-chaine, insensible a la casse). Repeatable.",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> None:
"""Point d'entree principal.
1. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis)
2. Cree le GiteaClient
3. Collecte les donnees via collect_all()
4. Affiche via render_dashboard()
5. Gere les erreurs : config manquante, connexion refusee, timeout
Args:
argv: Arguments CLI. Si None, utilise sys.argv (via argparse).
1. Parse les options CLI (--repo, --exclude)
2. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis)
3. Cree le GiteaClient
4. Collecte les donnees via collect_all() avec filtres
5. Affiche via render_dashboard()
6. Gere les erreurs : config manquante, connexion refusee, timeout
"""
args = parse_args(argv)
console = Console(stderr=True)
token = os.environ.get("GITEA_TOKEN")
@@ -38,7 +74,7 @@ def main() -> None:
client = GiteaClient(url, token)
try:
repos = collect_all(client)
repos = collect_all(client, include=args.repo, exclude=args.exclude)
except requests.ConnectionError:
console.print("[red]Erreur : connexion refusee. Verifiez l'URL et le serveur Gitea.[/red]")
sys.exit(1)

View File

@@ -22,13 +22,37 @@ class RepoData:
milestones: list[dict] # [{title, open_issues, closed_issues, due_on}]
def collect_all(client: GiteaClient) -> list[RepoData]:
"""Collecte les donnees de tous les repos.
def _matches_any(name: str, patterns: list[str]) -> bool:
"""Return True if name contains any of the patterns (case-insensitive)."""
name_lower = name.lower()
return any(p.lower() in name_lower for p in patterns)
Pour chaque repo : enrichit avec release et milestones.
Calcule open_issues = open_issues_count - open_pr_counter.
def collect_all(
client: GiteaClient,
include: list[str] | None = None,
exclude: list[str] | None = None,
) -> list[RepoData]:
"""Collecte les donnees des repos, avec filtrage optionnel.
Args:
client: Client API Gitea.
include: Si fourni, ne garde que les repos dont le nom contient
au moins une des sous-chaines (insensible a la casse).
exclude: Si fourni, exclut les repos dont le nom contient
au moins une des sous-chaines (insensible a la casse).
Ordre d'application : include d'abord (si present), puis exclude.
Si include est None ou vide, tous les repos sont inclus avant l'etape exclude.
"""
repos = client.get_repos()
# Filtrage post-fetch : l'API Gitea ne supporte pas le filtre par nom
if include:
repos = [r for r in repos if _matches_any(r["name"], include)]
if exclude:
repos = [r for r in repos if not _matches_any(r["name"], exclude)]
result: list[RepoData] = []
for repo in repos: