1 Commits
Author SHA1 Message Date
dsql 04ca4919e5 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-06 21:21:00 -04:00
3 changed files with 13 additions and 21 deletions
+3 -3
View File
@@ -13,16 +13,16 @@ 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@v1.0.2 aiowebhooks @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.8
# 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@v1.0.2 aiowebhooks[discord] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.8
``` ```
The base pulls `aiohttp` and `commons` (for the retry/backoff engine). Only 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 `aiowebhooks[discord]` adds `discord.py` (>=2.3, mainline, not discord.py-self), and
only for `DiscordWebhook`. only for `DiscordWebhook`.
Drop the `@v1.0.2` suffix from the line above to install the latest unpinned. Drop the `@v0.1.8` suffix from the line above to install the latest unpinned.
## Core sender ## Core sender
+2 -2
View File
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
[project] [project]
name = "aiowebhooks" name = "aiowebhooks"
version = "1.1.0" version = "1.0.0"
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+https://git.rethinkstudios.io/rethink-public/commons.git@v1.0.0", "commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.1",
] ]
[project.optional-dependencies] [project.optional-dependencies]
+8 -16
View File
@@ -141,10 +141,8 @@ class Webhook:
except _Retryable as exhausted: except _Retryable as exhausted:
return exhausted.result return exhausted.result
except Exception as error: except Exception as error:
# never-raises net: anything else (closed session, bad proxy url, ...) -> failed result. # never-raises net: anything else (closed session, bad proxy url, ...) -> failed result
# ERROR: an UNEXPECTED failure is swallowed here into a failed result (op failed, result log.warning("webhook send failed unexpectedly on %s: %s", url, error, exc_info=True)
# lost, a human may need to act) - the lib is the last code that sees this exception.
log.error("webhook send failed unexpectedly on %s: %s", url, error, exc_info=True)
return WebhookResult( return WebhookResult(
ok=False, status=None, url=url, attempts=counter[0] or 1, ok=False, status=None, url=url, attempts=counter[0] or 1,
error=f"{type(error).__name__}: {error}", error=f"{type(error).__name__}: {error}",
@@ -167,8 +165,7 @@ class Webhook:
if pending_wait[0] is not None: if pending_wait[0] is not None:
wait = pending_wait[0] wait = pending_wait[0]
pending_wait[0] = None pending_wait[0] = None
log.warning("webhook 429 on %s; honoring retry_after %.3fs (attempt %d/%d)", log.warning("webhook 429 on %s; honoring retry_after %.3fs", url, wait)
url, wait, counter[0] + 1, self.max_retries + 1)
await asyncio.sleep(wait) await asyncio.sleep(wait)
counter[0] += 1 counter[0] += 1
attempts = counter[0] attempts = counter[0]
@@ -177,11 +174,9 @@ class Webhook:
try: try:
proxy_dict = self._proxies.get() proxy_dict = self._proxies.get()
except Exception: except Exception:
# duck-typed provider: any get() error means no proxy available. # duck-typed provider: any get() error means no proxy available
# ERROR: swallowed into a failed result (op failed, result lost) - the log.warning("webhook: proxy get() failed; no proxy available",
# provider fault vanishes here unless it is logged loudly. exc_info=True)
log.error("webhook: proxy get() failed; no proxy available",
exc_info=True)
return WebhookResult( return WebhookResult(
ok=False, status=None, url=url, attempts=attempts, ok=False, status=None, url=url, attempts=attempts,
error="proxies unavailable", proxy=last_proxy, error="proxies unavailable", proxy=last_proxy,
@@ -214,8 +209,7 @@ class Webhook:
if wait is not None: if wait is not None:
pending_wait[0] = wait pending_wait[0] = wait
else: else:
log.warning("webhook 429 on %s; no retry_after, backing off (attempt %d/%d)", log.warning("webhook 429 on %s; no retry_after, backing off", url)
url, attempts, self.max_retries + 1)
raise _Retryable(result) raise _Retryable(result)
if status >= 500: if status >= 500:
raise _Retryable(result) raise _Retryable(result)
@@ -247,9 +241,7 @@ class Webhook:
self._proxies.burn(proxy) self._proxies.burn(proxy)
return True return True
except Exception: except Exception:
# ERROR: an unexpected provider fault (burn() raising) is swallowed here and ends log.warning("webhook: proxy burn failed; ending rotation", exc_info=True)
# rotation, failing the send - it must be loud, not a quiet WARNING.
log.error("webhook: proxy burn failed; ending rotation", exc_info=True)
return False return False
@staticmethod @staticmethod