repomatic.github.unsubscribe moduleΒΆ

Unsubscribe from closed, inactive GitHub notification threads.

Processes notification threads in two phases:

  1. REST notification threads: fetches the Issue/PullRequest notification threads last moved before the cutoff via /notifications, resolves each subject in batched GraphQL lookups, and unsubscribes the closed, stale ones via DELETE + PATCH.

  2. GraphQL threadless subscriptions: searches for closed issues/PRs the user is involved in but that lack notification threads, and unsubscribes via the updateSubscription mutation.

Requires the gh CLI to be installed and authenticated with a token that has the notifications scope (classic PAT) or equivalent fine-grained permissions.

repomatic.github.unsubscribe.GRAPHQL_PAGE_SIZE = 25ΒΆ

Per-page count for GraphQL search results.

repomatic.github.unsubscribe.NOTIFICATION_PAGE_SIZE = 50ΒΆ

Per-page count for REST /notifications results.

repomatic.github.unsubscribe.NOTIFICATION_SUBJECT_TYPES = frozenset({'Issue', 'PullRequest'})ΒΆ

Notification subject types to process.

repomatic.github.unsubscribe.SUBJECT_BATCH_SIZE = 50ΒΆ

Subjects looked up per GraphQL round trip.

Looked up one at a time, each thread costs a REST detail call. A batched lookup answers 50 in one request for a single rate-limit point, so a 113-thread pool costs three requests instead of 113.

repomatic.github.unsubscribe.SUBJECT_STATES = {'CLOSED': 'closed', 'MERGED': 'closed', 'OPEN': 'open'}ΒΆ

GraphQL subject states, mapped onto the REST spelling the phase compares.

REST reports a merged pull request as closed, where GraphQL distinguishes MERGED. Collapsing the two here keeps one vocabulary in the caller, and keeps the batched lookup a drop-in for the one-subject REST lookup, _get_thread_details.

repomatic.github.unsubscribe.SUBJECT_URL_PATTERN = re.compile('/repos/(?P<owner>[^/]+)/(?P<repo>[^/]+)/(?:issues|pulls)/(?P<number>\\d+)$')ΒΆ

Owner, repository and number, read off a notification’s subject URL.

A subject URL is the only handle the notification list gives out. GraphQL addresses the same object by coordinates, and issueOrPullRequest covers both shapes, so the issues/pulls half of the path is read and discarded.

repomatic.github.unsubscribe.UNSUBSCRIBE_WORKFLOW = 'unsubscribe.yaml'ΒΆ

Workflow file the backlog warning links to for a manual run with a higher cap.

The report renders inside that workflow’s own run, so the reader is one click from the Run workflow form the warning tells them to use. Kept equal to the filename repomatic.registry.COMPONENTS ships, since a link to a workflow a repository does not have is worse than no link.

class repomatic.github.unsubscribe.DetailRow(action, html_url, number, repo, title, updated_at)[source]ΒΆ

Bases: object

Per-item detail for the markdown report table.

action: ReportActionΒΆ
html_url: strΒΆ
number: int | NoneΒΆ
repo: strΒΆ
title: strΒΆ
updated_at: datetime | NoneΒΆ
class repomatic.github.unsubscribe.Phase1Result(cutoff=None, max_unsubscribes=0, newest_updated=None, oldest_updated=None, rows=<factory>, threads_deferred=0, threads_failed=0, threads_skipped_open=0, threads_skipped_recent=0, threads_skipped_unknown=0, threads_total=0, threads_unsubscribed=0)[source]ΒΆ

Bases: object

Accumulated counts and details from REST notification phase.

cutoff: datetime | None = NoneΒΆ
max_unsubscribes: int = 0ΒΆ
newest_updated: datetime | None = NoneΒΆ
oldest_updated: datetime | None = NoneΒΆ
rows: list[DetailRow]ΒΆ
threads_deferred: int = 0ΒΆ
threads_failed: int = 0ΒΆ
threads_skipped_open: int = 0ΒΆ
threads_skipped_recent: int = 0ΒΆ
threads_skipped_unknown: int = 0ΒΆ
threads_total: int = 0ΒΆ
threads_unsubscribed: int = 0ΒΆ
class repomatic.github.unsubscribe.Phase2Result(cutoff=None, items_deferred=0, items_failed=0, items_not_subscribed=0, items_skipped_recent=0, items_total=0, items_unsubscribed=0, max_unsubscribes=0, rows=<factory>, search_query='', skipped=False, skip_reason='')[source]ΒΆ

Bases: object

Accumulated counts and details from GraphQL threadless phase.

cutoff: datetime | None = NoneΒΆ
items_deferred: int = 0ΒΆ
items_failed: int = 0ΒΆ
items_not_subscribed: int = 0ΒΆ
items_skipped_recent: int = 0ΒΆ
items_total: int = 0ΒΆ
items_unsubscribed: int = 0ΒΆ
max_unsubscribes: int = 0ΒΆ
rows: list[DetailRow]ΒΆ
search_query: str = ''ΒΆ
skipped: bool = FalseΒΆ
skip_reason: str = ''ΒΆ
class repomatic.github.unsubscribe.UnsubscribeResult(dry_run=False, months=3, phase1=<factory>, phase2=<factory>)[source]ΒΆ

Bases: object

Accumulated results from both unsubscribe phases.

dry_run: bool = FalseΒΆ
months: int = 3ΒΆ
phase1: Phase1ResultΒΆ
phase2: Phase2ResultΒΆ
repomatic.github.unsubscribe.render_report(result, repo_url=None)[source]ΒΆ

Render a markdown report from unsubscribe results.

Pure function that produces the same markdown structure as the downstream unsubscribe.yaml workflow’s $GITHUB_STEP_SUMMARY. The repository is passed in rather than probed, so the renderer stays pure and the caller keeps the one environment lookup.

Parameters:
  • result (UnsubscribeResult) – Structured results from both phases.

  • repo_url (str | None) – Repository the run belongs to, linking the backlog warning to its workflow. Omitted, that warning renders as plain text.

Return type:

str

Returns:

Markdown report string.

repomatic.github.unsubscribe.unsubscribe_threads(months, max_unsubscribes, dry_run)[source]ΒΆ

Unsubscribe from closed, inactive notification threads.

Runs two phases, each behind its own runner:

  1. REST notification threads (_run_rest_phase): fetches notification threads, inspects each subject for closed + stale status, and unsubscribes.

  2. GraphQL threadless subscriptions (_run_graphql_phase): searches for closed issues/PRs the user is involved in and unsubscribes via mutation.

Parameters:
  • months (int) – Inactivity threshold in months.

  • max_unsubscribes (int) – Maximum unsubscribes per phase.

  • dry_run (bool) – If True, report what would be done without acting.

Return type:

UnsubscribeResult

Returns:

Structured results from both phases.