Skip to content

feat(providers): gemeinsame ProviderError-Basis + RateLimitError für einheitliche Fehler-/Rate-Limit-Erkennung #96

@Seretos

Description

@Seretos

#ai-generated

Problem

Die Provider-Abstraktion leakt die Fehler-Semantik. Jeder Provider wirft eine eigene, voneinander unabhängige Exception:

  • GitHubError(RuntimeError)providers/github.py, .status: int, .message
  • GitLabError(RuntimeError)providers/gitlab.py, .status, .message
  • AzureDevOpsError(RuntimeError)providers/azuredevops.py, .status

Es gibt keinen gemeinsamen Basistyp in providers/base.py (dort nur Relation-Exceptions) und keinen dedizierten Rate-Limit-Typ. Folgen für Consumer (workboard, agent-project-issues):

  • except <ProviderError> ist nicht möglich — man muss alle drei konkreten Typen importieren oder breit except Exception fangen.
  • „War das ein Rate-Limit und wann darf ich wieder?" lässt sich nur per .status == 403/429 plus String-Schnüffeln in .message (GitHub) beantworten — pro Provider unterschiedlich. Genau das Detail, das die Abstraktion kapseln sollte, liegt beim Consumer.

Konkret hat das im Workboard dazu geführt, dass die Rate-Limit-Behandlung toter Code war (sie fing httpx.HTTPStatusError, das real kein Provider wirft) und das Board bei Rate-Limit still „Keine offenen Tickets" zeigte. Siehe workboard-Ticket (Relation).

Vorschlag

In providers/base.py:

class ProviderError(RuntimeError):
    def __init__(self, status: int, message: str):
        super().__init__(message)
        self.status = status
        self.message = message

class RateLimitError(ProviderError):
    def __init__(self, status: int, message: str, retry_after: int | None = None):
        super().__init__(status, message)
        self.retry_after = retry_after  # Sekunden bis Reset, falls bekannt
  • GitHubError / GitLabError / AzureDevOpsError erben von ProviderError (Signatur/.status/.message bleiben → rückwärtskompatibel, bestehende except GitHubError funktionieren weiter).
  • Jeder _check() wirft RateLimitError statt des generischen Provider-Errors, wenn er ein Rate-Limit erkennt, und füllt retry_after:
    • GitHub: 403 + Header x-ratelimit-remaining == "0"retry_after = x-ratelimit-reset - now (Sekunden). (Logik existiert bereits halb in github.py::_check, nur als Message-Präfix.)
    • GitLab: 429Retry-After bzw. RateLimit-Reset.
    • Azure: 429Retry-After.

Akzeptanzkriterien

  • ProviderError + RateLimitError in base.py, die drei Provider-Errors erben davon.
  • Pro Provider ein Test: Rate-Limit-Response → RateLimitError mit korrektem retry_after; gewöhnlicher Fehler → konkreter ProviderError-Subtyp mit .status.
  • RateLimitError ist auch ein ProviderError und (für Kompat) weiterhin ein RuntimeError.
  • Export über providers/__init__.py bzw. Package-Top-Level, sodass Consumer from lib_python_projects.providers.base import ProviderError, RateLimitError nutzen können.

Downstream (eigene Tickets)

  • workboard konsumiert RateLimitError/ProviderError statt httpx.HTTPStatusError + .status-Sniffing.
  • agent-project-issues — vermutlich No-op (surfacet Fehler schon via _safe), Verify-Ticket.

Optional als Folge-Ticket: ETag/If-None-Match-Conditional-Requests im Provider-Layer (GitHub 304 Not Modified zählt nicht gegen das Primär-Rate-Limit) — größter Hebel für pollende Consumer auf einem geteilten Token.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-generatedCreated by the project-issues AI agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions