Files
kosmos 5f4487f3ad feat(config): menu-driven multi-account wizard, v2 schema, config-time Launchpad OAuth
Address review of PR #1 (Valentin): replace the linear config walk-through
and single-section schema.

- weekly-activity config becomes a menu loop: Current sources (masked) ->
  Add a new source (pick kind, repeatable) / Remove a source / Exit; the
  file is written only when something changed
- new versioned config layout (version = 2): [[kind.accounts]] arrays so
  several accounts or instances per provider coexist; optional display
  labels; strict unknown-key/type validation kept (errors carry account
  position, e.g. [github.accounts[1]])
- legacy v1 files (no version key, one section per source) migrate
  transparently in memory on load; saving from the wizard persists v2;
  the file itself is never rewritten by load_config
- non-anonymous Launchpad accounts now authenticate at config time:
  constructing LaunchpadSource runs launchpadlib's browser OAuth and the
  token lands in the system keyring before any report; an explicit
  credentials_file stays supported as an additive opt-out
- report aggregates every configured account in stable order with failure
  isolation intact; headings add the account label when it disambiguates
  (source_label shared between aggregate and wizard)
- GitHub accounts gain optional token_env resolved lazily against the
  environment before falling back to GITHUB_TOKEN / `gh auth token`
2026-08-26 22:53:17 +00:00

173 lines
6.9 KiB
Markdown

# weekly-activity
Aggregate weekly activity across development platforms (Launchpad, GitHub, GitLab, Debian BTS) into a single text report.
## Usage
```bash
# Interactive setup: pick sources, store identities/tokens once
uv run weekly-activity config
# Aggregated report across every configured source
uv run weekly-activity report # bare `weekly-activity` does the same
```
### Single sources (ad-hoc, no config needed)
```bash
# Launchpad (uses system keyring for OAuth; --anonymous for public data only)
uv run weekly-activity launchpad <username>
uv run weekly-activity launchpad --anonymous <username>
# GitHub (uses GITHUB_TOKEN env var or `gh auth token`; anonymous works but rate-limited)
uv run weekly-activity github <username>
uv run weekly-activity github --token ghp_xxx <username>
# GitLab (gitlab.com by default; any self-hosted instance via --url, e.g. Debian Salsa;
# uses GITLAB_TOKEN / SALSA_TOKEN env vars, anonymous works but rate-limited)
uv run weekly-activity gitlab <username>
uv run weekly-activity gitlab --url https://salsa.debian.org <username>
uv run weekly-activity gitlab --url https://salsa.debian.org --token glpat-xxx <username>
# Debian BTS (bugs.debian.org; identify by the email used on bugs)
uv run weekly-activity bts <email>
# Custom date range (ISO dates, UTC)
uv run weekly-activity launchpad --since 2025-08-01 --until 2025-08-15 <username>
uv run weekly-activity github --since 2025-08-01 --until 2025-08-15 <username>
uv run weekly-activity gitlab --since 2025-08-01 --until 2025-08-15 <username>
uv run weekly-activity bts --since 2025-08-01 --until 2025-08-15 <email>
```
### GitLab instances
Any GitLab instance works via `--url`. Token discovery order:
1. `--token` flag
2. env var named by `--token-env`
3. `<HOST>_TOKEN` derived from the instance host (e.g. `SALSA_TOKEN` for `salsa.debian.org`)
4. `GITLAB_TOKEN`
## What it reports
| Section | Launchpad | GitHub | GitLab | Debian BTS |
|---|---|---|---|---|
| Merge proposals / PRs authored | `getMergeProposals` (created in range) | `/search/issues` (`author:` + `type:pr`) | `/merge_requests` (`author_id`, created range) | — |
| Review requests received | `getRequestedReviews` (filtered by `date_review_requested`) | `/search/issues` (`reviewed-by:` + `type:pr`) | — | — |
| Reviews performed | `getRequestedReviews` (filtered by `date_reviewed`) | (same query) | events (`action=approved`) | — |
| Issues / Bugs | `searchTasks` (reporter, commenter, assignee) | `/search/issues` (author, commenter, assignee) | `/issues` (`author_id`, `assignee_id`) | `get_bugs` (`submitter`) + `get_status` (created in range) |
| Bugs owned, updated | — | — | — | `get_bugs` (`owner`) + `get_status` (`log_modified`) |
| Repositories modified | `git_repositories.getRepositories` (`modified_since_date`) | `/users/{user}/repos` (filtered by `pushed_at`) | — (covered by push events) | — |
| Commits pushed | — (not in Launchpad REST API) | `/repos/{owner}/{repo}/commits` (per active repo) | events (`action=pushed`) | — |
### Default time window
Rolling 7 days ending today (inclusive). Override with `--since` / `--until` (ISO dates, UTC).
## Configuration
`weekly-activity config` opens a small menu-driven editor: it lists your
current sources (secrets masked), then lets you **Add a new source**
(choose the kind, repeat as often as you like), **Remove a source**, or
**Exit** (writes the file only when something changed). Several accounts
of the same provider can live side by side. Tokens are stored as provided
(plaintext), but the file is created with owner-only permissions (`0600`)
and secrets are never echoed back. Use `--config <path>` (also accepted by
`report`) to keep configs elsewhere.
Default location: `${XDG_CONFIG_HOME:-~/.config}/weekly-activity.toml`.
```toml
version = 2
[[launchpad.accounts]]
username = "jane"
mode = "credentials" # "anonymous" (public data only) | "credentials"
# credentials_file = "~/lp-creds.json" # omit -> system keyring
[[github.accounts]]
name = "personal" # optional display label
username = "octocat"
token = "" # empty -> $token_env / GITHUB_TOKEN / `gh auth token`
[[github.accounts]]
name = "work"
username = "acme-jane"
token_env = "WORK_GH_TOKEN"
[[gitlab.accounts]]
name = "salsa"
url = "https://salsa.debian.org"
username = "jane"
token_env = "SALSA_TOKEN"
[[bts.accounts]]
email = "jane@example.org"
```
An absent section disables that source; an empty one is not written.
When a non-anonymous Launchpad account is added without a credentials
file, the wizard runs launchpadlib's OAuth right there — a browser opens
once and the token is kept in your system keyring, so `report` never has
to authenticate.
Legacy configs without the `version` key (one single-account section per
source) keep working: they are migrated transparently when loaded and
rewritten in the v2 shape the next time the wizard saves.
## Architecture
```
src/weekly_activity/
model.py ActivityReport, Section, last_week_window
report.py format_report() — source-agnostic text renderer
cli.py subcommand dispatch (launchpad | github | gitlab | bts | report | config)
aggregate.py collect_specs()/format_combined() — one text report across sources
config.py ActivityConfig schema + TOML load/save (tomllib read, hand-written)
wizard.py interactive `config` onboarding
sources/
__init__.py ActivitySource protocol
launchpad.py LaunchpadSource (launchpadlib)
github.py GitHubSource (httpx)
gitlab.py GitLabSource (httpx, REST v4 — gitlab.com or self-hosted)
debian_bts.py DebianBtsSource (python-debianbts SOAP)
```
Each source implements `ActivitySource` — a `name` and a `collect(username, since, until) -> ActivityReport` method. Sources keep their own dataclasses internally; the shared contract is `Section` (title + pre-formatted lines) and `ActivityReport` (metadata + sections + warnings). The report formatter (`report.py`) is source-agnostic.
Per-query failures (rate limits, server timeouts) are caught per-section — a failing endpoint records a warning and degrades to an empty section rather than killing the whole report.
## Development
```bash
uv sync # install deps + dev tooling
uv run ruff format src # format
uv run ruff check src # lint
uv run ty check # type check
```
## CI / releases
Gitea Actions (`.gitea/workflows/ci.yml`) runs two jobs:
- **check** — format, lint, and type check on every push to `main`
- **release** — on `v*` tags: builds a standalone Linux executable with
PyInstaller, smoke-tests it, creates a Gitea release, and uploads the
binary as an asset
Cut a release with:
```bash
git tag vX.Y.Z && git push origin vX.Y.Z
```
## Dependencies
- **launchpadlib** — Launchpad REST API client
- **httpx** — GitHub & GitLab REST API client
- **python-debianbts** — Debian BTS SOAP client
- **ruff** — format + lint (dev)
- **ty** — type check (dev)