10 Commits
Author SHA1 Message Date
dsql fb382f9767 release: 1.0.0
first stable release. pre-1.0.0 verification complete: all surviving MED regressions and
gaps resolved and independently re-fired, tree audited clean across the suite.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-09 18:53:15 -04:00
dsql 0d9d93e4c1 docs: compress residual internal docstrings
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-03 16:48:19 -04:00
dsql 7a157efc16 docs: compress prose/module docstrings, em-dash->hyphen (de-bloat wave 1)
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-03 00:15:08 -04:00
dsql 6d4183948e fix: no sleep on exhausted final 429 attempt; correct errors.py doc (v0.1.6)
A 429 retry_after was slept immediately, including on the final attempt
that goes on to exhaust max_retries — a pointless wait right before
giving up. The wait is now carried in a per-call mutable cell and slept
at the start of the next attempt instead, so it's honored before every
attempt that actually runs and never after the last one.

errors.py's docstring claimed the exported error types cover the
missing-[discord]-extra raise path; that raise is a plain RuntimeError,
not one of these types. Reworded to match.

Compressed essay-length docstrings/comments across sender.py; no
behavior change. Verified against the aioproxies twin: aiowebhooks'
proxy-key normalization already routes zero-padded ports through
urlsplit().port (parses to int, no zero-pad on render), so host:080
and host:80 already collapse to one canonical key — no code change
needed there.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-02 23:23:24 -04:00
dsql 90f67cf9fa fix: bound 429 retry_after and retry connection errors without a proxy (v0.1.5)
A server-controlled 429 retry_after/Retry-After was slept verbatim with no
finiteness check or ceiling, so an inf or huge value (adversarial or a
ms-vs-s unit mismatch) could stall send() for hours outside max_retries
accounting. Non-finite values are now rejected and finite values clamped
to MAX_RETRY_AFTER (300s).

Connection/timeout errors with no proxy provider set failed one-shot,
contradicting the README's documented "normal retry" behavior and skipping
the single most retry-worthy failure class. They now raise the internal
_Retryable signal so commons.aretry retries them under max_retries, same
as a 5xx, while still returning a failed WebhookResult (never raising)
once retries are exhausted.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-02 17:08:14 -04:00
dsql ef20bc51f0 fix: never-raises net widened to unexpected exceptions; changelog/429 docs (v0.1.4)
M-2: _attempt caught only (aiohttp.ClientError, asyncio.TimeoutError); an unexpected
error escaping the attempt (closed injected session -> RuntimeError, malformed proxy url
-> ValueError) propagated out of send(), breaking the documented 'never raises on a send
failure' contract. add an outer catch-all in _send_loop converting any such exception to a
falsy WebhookResult(ok=False), logged at warning with exc_info.

aiowebhooks-F3: README 429 section + changelog were stale vs the v0.1.3 'retry any 429'
fix; added the no-parseable-wait-still-retries wording and v0.1.3/v0.1.4 changelog entries.

verified by execution: closed-session (RuntimeError) and bad-proxy (ValueError) controls
both fire and now return ok=False instead of raising.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 20:47:30 -04:00
dsql f3d2561bf9 fix: retry any 429, not just those with a parseable retry_after (v0.1.3)
treat every status==429 as retryable: sleep only when retry_after parses, but raise
_Retryable either way so aretry's backoff + max_retries cap engages. previously a 429
with no body retry_after and no Retry-After header (edge/Cloudflare/generic webhook)
returned a terminal ok=False with no retry, contradicting the documented retry-on-429.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-29 17:09:30 -04:00
dsql aec0a5cc2b 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>
2026-06-28 16:16:13 -04:00
dsql 40f8cc5b5f fix: never-raises contract + retry migration to commons.aretry (v0.1.1)
- seam bug: _burn/get() only caught ProxiesExhaustedError, but aioproxies.burn()
  raises ValueError ('proxy not in pool') which escaped send() and broke the
  'never raises on send failure' contract. catch ANY exception across the
  duck-typed provider seam and convert to a failed WebhookResult.
- 5xx hot loop: 5xx retries had no backoff (immediate retry, hammering the
  endpoint). migrate 429/5xx retry onto commons.aretry (>=0.2.0) for correct
  exponential backoff + cap.
- lost response: exhausted retries returned a synthetic status-0 result; now the
  real last 4xx/5xx status + body is returned (aretry re-raises the carried
  _Retryable, the loop unwraps it).

verified by execution: burn/get ValueError no longer escapes, 5xx backs off
(~1.9s over 3 retries vs ~0s hot loop), exhausted 5xx returns real 503 + body,
429 retry_after honored, 4xx/rotation/round-robin intact.

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-27 21:43:41 -04:00
dsql 7a0c8abf30 add package: pyproject + src (core Webhook, WebhookResult, errors, discord layer)
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-25 14:50:05 -04:00