fix: remove dead clock param; pin commons v0.2.1 (v0.1.2)

the clock= constructor param was stored (self._clock) but never read — the 429
retry_after wait uses asyncio.sleep directly. it was dead code, and the CLAUDE.md
wrongly claimed it made 429 timing test-controllable. remove the param + the unused
time import, and correct the doc (tests patch commons.retry's sleep + sender.asyncio
.sleep, not a clock seam). bump the commons pin to v0.2.1 (retry attempts floor).

verified: clock param gone, constructs fine, 18/18 fix harness intact.
Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
disqualifier 2026-06-28 16:13:50 -04:00
parent 40f8cc5b5f
commit aec0a5cc2b
4 changed files with 40 additions and 22 deletions

View File

@ -13,13 +13,14 @@ send to the core — inheriting rotation, proxy, retry, and result for free.
## Install ## 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: # 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, The base pulls `aiohttp` and `commons` (for the retry/backoff engine). Only
mainline — not discord.py-self), and only for `DiscordWebhook`. `aiowebhooks[discord]` adds `discord.py` (>=2.3, mainline — not discord.py-self), and
only for `DiscordWebhook`.
## Core sender ## Core sender
@ -65,12 +66,15 @@ result.proxy # canonical proxy string used (host:port:user:pass / host:port)
## Retry & rate limits ## Retry & rate limits
- **429** — waits the `retry_after` from the body first (Discord sends seconds), then Status retries run through `commons.aretry` (exponential backoff + cap):
the `Retry-After` header, then retries; capped by `max_retries`.
- **5xx** — retried, capped by `max_retries`. - **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`. - **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) ## 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 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 `max_proxy_retries`. Hitting the cap, or **any exception from the provider's
failed result — never an infinite loop. With no provider, a timeout just fails after `get()`/`burn()`** (the provider is duck-typed and never imported, so its exception
normal retry. 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]`) ## 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 - Every send returns a `WebhookResult`; the core never raises on a send failure and
never prints. Callers check `result.ok`. never prints. Callers check `result.ok`.
- v0.1.0 is JSON-only: **files/attachments, `tts`, and `allowed_mentions` are out** - JSON-only: **files/attachments, `tts`, and `allowed_mentions` are out** (deliberate
(deliberate scope cut, addable later). The Discord surface is content + embeds + scope cut, addable later). The Discord surface is content + embeds + identity.
identity. - Rotation is round-robin only; try-next-on-failure across URLs is a later feature.
- v0.1.0 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 ## Versioning

View File

@ -4,12 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "aiowebhooks" 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" description = "async webhook sender (aiohttp) with round-robin urls, retry, and proxy rotation; optional discord.py embeds"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"aiohttp>=3.9", "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] [project.optional-dependencies]

View File

@ -21,4 +21,4 @@ from .sender import Webhook
__all__ = ["Webhook", "WebhookResult", "WebhookError", "NoUrlsError"] __all__ = ["Webhook", "WebhookResult", "WebhookError", "NoUrlsError"]
__version__ = "0.1.1" __version__ = "0.1.2"

View File

@ -8,7 +8,6 @@ the actual POST here so it inherits rotation / proxy / retry / result.
import asyncio import asyncio
import logging import logging
import time
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from urllib.parse import unquote, urlsplit from urllib.parse import unquote, urlsplit
@ -72,7 +71,6 @@ class Webhook:
timeout: float = 15, timeout: float = 15,
max_retries: int = 3, max_retries: int = 3,
max_proxy_retries: int = 3, max_proxy_retries: int = 3,
clock=time.monotonic,
): ):
self._urls = [urls] if isinstance(urls, str) else list(urls) self._urls = [urls] if isinstance(urls, str) else list(urls)
if not self._urls: if not self._urls:
@ -82,7 +80,6 @@ class Webhook:
self.timeout = timeout self.timeout = timeout
self.max_retries = max_retries self.max_retries = max_retries
self.max_proxy_retries = max_proxy_retries self.max_proxy_retries = max_proxy_retries
self._clock = clock
self._index = 0 self._index = 0
def _next_url(self) -> str: def _next_url(self) -> str: