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
52 lines
1.6 KiB
Python
52 lines
1.6 KiB
Python
"""Decide whether an assigned issue is agent-targeted, and for whom.
|
|
|
|
An issue is forge-targeted when BOTH hold:
|
|
- it carries a `bot-bottle:<agent>` label naming the agent, and
|
|
- at least one assignee is a member of the configured org.
|
|
|
|
An optional `bot-bottle-bottle:<name>` label overrides bottle selection.
|
|
The forge is duck-typed: any object with `is_org_member(org, user)`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Protocol
|
|
|
|
from .config import BOTTLE_LABEL_PREFIX, LABEL_PREFIX
|
|
from .model import IssueAssigned
|
|
|
|
|
|
class Membership(Protocol):
|
|
def is_org_member(self, org: str, username: str) -> bool: ...
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Target:
|
|
agent_name: str
|
|
bottle_override: str | None
|
|
|
|
|
|
def parse_labels(labels: tuple[str, ...]) -> tuple[str | None, str | None]:
|
|
"""Return (agent_name, bottle_override) parsed from labels."""
|
|
agent: str | None = None
|
|
bottle: str | None = None
|
|
for label in labels:
|
|
if label.startswith(BOTTLE_LABEL_PREFIX):
|
|
bottle = label[len(BOTTLE_LABEL_PREFIX):] or None
|
|
elif label.startswith(LABEL_PREFIX):
|
|
agent = label[len(LABEL_PREFIX):] or None
|
|
return agent, bottle
|
|
|
|
|
|
def resolve_target(
|
|
event: IssueAssigned, forge: Membership, org: str
|
|
) -> Target | None:
|
|
"""Return the `Target` for a forge-targeted issue, or None to ignore."""
|
|
agent, bottle = parse_labels(event.labels)
|
|
if not agent:
|
|
return None
|
|
if not any(forge.is_org_member(org, a) for a in event.assignees):
|
|
return None
|
|
return Target(agent_name=agent, bottle_override=bottle)
|