runv-server

server tooling for runv.club
Log | Files | Refs | README

runv-who (4126B)


      1 #!/usr/bin/env python3
      2 """Lista membros da runv.club."""
      3 
      4 from __future__ import annotations
      5 
      6 import argparse
      7 import json
      8 import sys
      9 from pathlib import Path
     10 from typing import Any
     11 
     12 sys.tracebacklimit = 0
     13 
     14 
     15 def _bootstrap() -> None:
     16     from pathlib import Path
     17 
     18     installed = Path("/usr/local/share/runv/lib")
     19     candidates = [installed]
     20     script = Path(__file__).resolve()
     21     if script.parent.name == "bin":
     22         candidates.insert(0, script.parent.parent / "lib")
     23     for c in candidates:
     24         if (c / "runv_community.py").is_file() and str(c) not in sys.path:
     25             sys.path.insert(0, str(c))
     26             return
     27 
     28 
     29 _bootstrap()
     30 import runv_community as rc  # noqa: E402
     31 
     32 
     33 def collect_member(username: str) -> dict[str, Any]:
     34     paths = rc.home_paths(username)
     35     try:
     36         has_homepage = rc.path_is_file(paths["public_index"])
     37         homepage_mtime = (
     38             rc.homepage_mtime_iso(paths["public_index"]) if has_homepage else None
     39         )
     40     except OSError:
     41         has_homepage = False
     42         homepage_mtime = None
     43     return {
     44         "username": username,
     45         "homepage": f"/~{username}/",
     46         "has_homepage": has_homepage,
     47         "homepage_mtime": homepage_mtime,
     48         "has_plan": rc.file_has_content(paths["plan"]),
     49         "has_project": rc.file_has_content(paths["project"]),
     50     }
     51 
     52 
     53 def sort_members(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
     54     with_home = [r for r in rows if r["has_homepage"]]
     55     without_home = [r for r in rows if not r["has_homepage"]]
     56 
     57     def mtime_key(r: dict[str, Any]) -> str:
     58         return r.get("homepage_mtime") or ""
     59 
     60     with_home.sort(key=lambda r: mtime_key(r), reverse=True)
     61     without_home.sort(key=lambda r: r["username"].lower())
     62     return with_home + without_home
     63 
     64 
     65 def filter_active(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
     66     return [
     67         r
     68         for r in rows
     69         if r["has_homepage"] or r["has_plan"] or r["has_project"]
     70     ]
     71 
     72 
     73 def print_text_table(rows: list[dict[str, Any]]) -> None:
     74     print("Membros runv.club\n")
     75     if not rows:
     76         print("(nenhum membro encontrado)")
     77         return
     78     for r in rows:
     79         user = r["username"]
     80         if r["has_homepage"]:
     81             hm = rc.format_mtime_local(r.get("homepage_mtime")) or "?"
     82             home_col = f"home {hm}"
     83         else:
     84             home_col = "sem homepage"
     85         flags = []
     86         if r["has_plan"]:
     87             flags.append(".plan")
     88         if r["has_project"]:
     89             flags.append(".project")
     90         flag_col = " ".join(flags) if flags else "-"
     91         print(f"{user:<12} {home_col:<22} {flag_col:<14} {r['homepage']}")
     92 
     93 
     94 def main(argv: list[str] | None = None) -> int:
     95     try:
     96         p = argparse.ArgumentParser(
     97             prog="runv-who",
     98             description="Lista membros da runv.club.",
     99         )
    100         p.add_argument("--json", action="store_true", help="saída em JSON")
    101         p.add_argument("--limit", type=int, default=None, metavar="N", help="limitar resultados")
    102         p.add_argument(
    103             "--active",
    104             action="store_true",
    105             help="só utilizadores com homepage, .plan ou .project",
    106         )
    107         args = p.parse_args(argv)
    108 
    109         if args.limit is not None and args.limit < 1:
    110             rc.friendly_exit("--limit deve ser um inteiro positivo.")
    111 
    112         names, warning = rc.load_member_usernames(rc.DEFAULT_USERS_JSON, rc.DEFAULT_HOME_ROOT)
    113         if warning:
    114             print(warning, file=sys.stderr)
    115 
    116         rows = [collect_member(u) for u in names]
    117         if args.active:
    118             rows = filter_active(rows)
    119         rows = sort_members(rows)
    120         if args.limit is not None:
    121             rows = rows[: args.limit]
    122 
    123         if args.json:
    124             print(json.dumps(rows, ensure_ascii=False, indent=2))
    125             return 0
    126 
    127         print_text_table(rows)
    128         return 0
    129     except KeyboardInterrupt:
    130         print("\nInterrompido.", file=sys.stderr)
    131         return 130
    132     except SystemExit:
    133         raise
    134     except Exception as e:
    135         rc.friendly_exit(f"erro: {e}")
    136 
    137 
    138 if __name__ == "__main__":
    139     raise SystemExit(main())