diff --git a/README.md b/README.md index 505691d..968ab3c 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,14 @@ send to the core — inheriting rotation, proxy, retry, and result for free. ## Install ``` -aiowebhooks @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.0 +aiowebhooks @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.1 # discord embeds / identity helpers need the extra: -aiowebhooks[discord] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.0 +aiowebhooks[discord] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.1 ``` -The base pulls `aiohttp`. Only `aiowebhooks[discord]` adds `discord.py` (>=2.3, -mainline — not discord.py-self), and only for `DiscordWebhook`. +The base pulls `aiohttp` and `commons` (for the retry/backoff engine). Only +`aiowebhooks[discord]` adds `discord.py` (>=2.3, mainline — not discord.py-self), and +only for `DiscordWebhook`. ## Core sender @@ -65,12 +66,15 @@ result.proxy # canonical proxy string used (host:port:user:pass / host:port) ## Retry & rate limits -- **429** — waits the `retry_after` from the body first (Discord sends seconds), then - the `Retry-After` header, then retries; capped by `max_retries`. -- **5xx** — retried, capped by `max_retries`. +Status retries run through `commons.aretry` (exponential backoff + cap): + +- **429** — honors the `retry_after` from the body first (Discord sends seconds), then + the `Retry-After` header, sleeping that exact value; capped by `max_retries`. +- **5xx** — retried with exponential backoff, capped by `max_retries`. - **4xx** (other than 429) — fails immediately (no retry), returned as `ok=False`. -Exceeding a cap returns a failed result rather than looping. +Exceeding a cap returns a failed result rather than looping — and the result carries the +**real** last status/body (not a synthetic placeholder). ## Proxy rotation (optional, duck-typed) @@ -91,9 +95,10 @@ result = await wh.send(payload) # sends through pm.get(); on a timeout/connect ``` On a timeout/connection error the current proxy is burned and the next is tried, up to -`max_proxy_retries`. A provider `ProxiesExhaustedError` (or hitting the cap) returns a -failed result — never an infinite loop. With no provider, a timeout just fails after -normal retry. +`max_proxy_retries`. Hitting the cap, or **any exception from the provider's +`get()`/`burn()`** (the provider is duck-typed and never imported, so its exception +types can't be caught by class), returns a failed result — never an infinite loop, never +an escape. With no provider, a timeout just fails after normal retry. ## Discord (`aiowebhooks[discord]`) @@ -124,11 +129,27 @@ Without the extra installed, importing `aiowebhooks` still works; constructing o - Every send returns a `WebhookResult`; the core never raises on a send failure and never prints. Callers check `result.ok`. -- v0.1.0 is JSON-only: **files/attachments, `tts`, and `allowed_mentions` are out** - (deliberate scope cut, addable later). The Discord surface is content + embeds + - identity. -- v0.1.0 rotation is round-robin only; try-next-on-failure across URLs is a later - feature. +- JSON-only: **files/attachments, `tts`, and `allowed_mentions` are out** (deliberate + scope cut, addable later). The Discord surface is content + embeds + identity. +- Rotation is round-robin only; try-next-on-failure across URLs is a later feature. + +## Changelog + +### v0.1.2 + +- Removed a dead `clock` constructor param (it was stored but never used). Pinned + `commons` to v0.2.1. + +### v0.1.1 + +- **Never-raises contract hardened:** an error from a duck-typed proxy provider's + `get()`/`burn()` (e.g. `aioproxies.burn()` raising `ValueError` for an unknown proxy) + used to escape `send()`. Now any provider exception is caught and converted to a + failed result. +- **Retry via `commons.aretry`:** 429/5xx retry moved onto the shared backoff engine — + 5xx now backs off (was a tight no-backoff loop), and exhausted retries return the + **real** last status/body instead of a synthetic placeholder. Adds a `commons` + dependency. ## Versioning diff --git a/pyproject.toml b/pyproject.toml index 9f55fc6..7f4b64a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "hatchling.build" [project] name = "aiowebhooks" -version = "0.1.1" +version = "0.1.2" description = "async webhook sender (aiohttp) with round-robin urls, retry, and proxy rotation; optional discord.py embeds" requires-python = ">=3.10" dependencies = [ "aiohttp>=3.9", - "commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.0", + "commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.1", ] [project.optional-dependencies] diff --git a/src/aiowebhooks/__init__.py b/src/aiowebhooks/__init__.py index e00ce4b..c212c69 100644 --- a/src/aiowebhooks/__init__.py +++ b/src/aiowebhooks/__init__.py @@ -21,4 +21,4 @@ from .sender import Webhook __all__ = ["Webhook", "WebhookResult", "WebhookError", "NoUrlsError"] -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/src/aiowebhooks/sender.py b/src/aiowebhooks/sender.py index 9cc873d..33c4406 100644 --- a/src/aiowebhooks/sender.py +++ b/src/aiowebhooks/sender.py @@ -8,7 +8,6 @@ the actual POST here so it inherits rotation / proxy / retry / result. import asyncio import logging -import time from typing import Dict, List, Optional, Union from urllib.parse import unquote, urlsplit @@ -72,7 +71,6 @@ class Webhook: timeout: float = 15, max_retries: int = 3, max_proxy_retries: int = 3, - clock=time.monotonic, ): self._urls = [urls] if isinstance(urls, str) else list(urls) if not self._urls: @@ -82,7 +80,6 @@ class Webhook: self.timeout = timeout self.max_retries = max_retries self.max_proxy_retries = max_proxy_retries - self._clock = clock self._index = 0 def _next_url(self) -> str: