314dc03b0d
Moves the orchestrator into bot_bottle/orchestrator/ so one install gets everything. Entry point is now `python -m bot_bottle.orchestrator run`. - Add bot_bottle/orchestrator/ with all 14 modules (verbatim move; internal imports were already relative, so no changes inside orchestrator modules) - Rewrite bootstrap.py: remove the lazy bot_bottle import guard, use direct relative imports from ..contrib.* - Add bot_bottle/contrib/forge/base.py: ScopedForge (read-anywhere / write-scoped) - Add bot_bottle/contrib/gitea/client.py: GiteaClient + GiteaForge (urllib.request only) - Add bot_bottle/contrib/gitea/forge_state.py: ForgeState + SqliteForgeStateStore - Add tests/unit/orchestrator/ (82 tests: 63 migrated + 19 new for contrib modules) Closes #321
86 lines
2.8 KiB
Python
86 lines
2.8 KiB
Python
"""Parse Gitea webhook payloads into typed `ForgeEvent`s.
|
|
|
|
Only the fields the orchestrator acts on are extracted; unknown payloads
|
|
and event types return None so the webhook layer can ignore them.
|
|
|
|
Gitea sends the event kind in the `X-Gitea-Event` header and the payload
|
|
as JSON. The relevant kinds:
|
|
|
|
- `issues` with `action == "assigned"` -> IssueAssigned
|
|
- `issue_comment` with `action == "created"` -> CommentCreated
|
|
- `pull_request` with `action == "closed"` -> PullRequestClosed
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from .model import CommentCreated, ForgeEvent, IssueAssigned, PullRequestClosed
|
|
|
|
|
|
def _repo_owner(payload: dict[str, Any]) -> tuple[str, str]:
|
|
repo = payload.get("repository") or {}
|
|
owner = (repo.get("owner") or {}).get("login", "")
|
|
return str(owner), str(repo.get("name", ""))
|
|
|
|
|
|
def parse_event(event_kind: str, payload: dict[str, Any]) -> ForgeEvent | None:
|
|
"""Map (X-Gitea-Event, payload) to a `ForgeEvent`, or None to ignore."""
|
|
if event_kind == "issues":
|
|
return _parse_issue(payload)
|
|
if event_kind == "issue_comment":
|
|
return _parse_comment(payload)
|
|
if event_kind == "pull_request":
|
|
return _parse_pull_request(payload)
|
|
return None
|
|
|
|
|
|
def _parse_issue(payload: dict[str, Any]) -> IssueAssigned | None:
|
|
if payload.get("action") != "assigned":
|
|
return None
|
|
owner, repo = _repo_owner(payload)
|
|
issue = payload.get("issue") or {}
|
|
assignees = tuple(
|
|
str(a.get("login", "")) for a in (issue.get("assignees") or [])
|
|
)
|
|
labels = tuple(str(l.get("name", "")) for l in (issue.get("labels") or []))
|
|
return IssueAssigned(
|
|
owner=owner,
|
|
repo=repo,
|
|
issue_number=int(issue.get("number", 0)),
|
|
title=str(issue.get("title", "")),
|
|
body=str(issue.get("body", "") or ""),
|
|
assignees=assignees,
|
|
labels=labels,
|
|
)
|
|
|
|
|
|
def _parse_comment(payload: dict[str, Any]) -> CommentCreated | None:
|
|
if payload.get("action") != "created":
|
|
return None
|
|
owner, repo = _repo_owner(payload)
|
|
issue = payload.get("issue") or {}
|
|
comment = payload.get("comment") or {}
|
|
return CommentCreated(
|
|
owner=owner,
|
|
repo=repo,
|
|
issue_number=int(issue.get("number", 0)),
|
|
comment_id=int(comment.get("id", 0)),
|
|
author=str((comment.get("user") or {}).get("login", "")),
|
|
body=str(comment.get("body", "") or ""),
|
|
is_pull=bool(issue.get("pull_request")),
|
|
)
|
|
|
|
|
|
def _parse_pull_request(payload: dict[str, Any]) -> PullRequestClosed | None:
|
|
if payload.get("action") != "closed":
|
|
return None
|
|
owner, repo = _repo_owner(payload)
|
|
pr = payload.get("pull_request") or {}
|
|
return PullRequestClosed(
|
|
owner=owner,
|
|
repo=repo,
|
|
pr_number=int(pr.get("number", 0)),
|
|
merged=bool(pr.get("merged", False)),
|
|
)
|