feat: fold bot-bottle-orchestrator into bot_bottle/orchestrator subpackage
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
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user