hermesi
Server-side client for Hermesi: publish events, mint subscriber tokens and preference links. Thin on purpose: it builds the request, sends it with retries, and turns the answer into a typed result or an exception. It makes no decision about notifications; that is the platform's job.
- Retries on connection failures, timeouts,
429(honouringRetry-After) and5xx, with exponential backoff and jitter. - Idempotency built in. Every event goes out with an
Idempotency-Key(generated if you give none) that is kept across the retries, so a lost response cannot send a notification twice. - Typed, with a
py.typedmarker, for synchronous and asynchronous code (Hermesi,AsyncHermesi). - A test mode that sends nothing and records what you would have sent.
- Python 3.9 and later. One dependency,
httpx.
Install
pip install hermesi
Publish an event
import os
from hermesi import Hermesi
hermesi = Hermesi(
api_key=os.environ["HERMESI_SECRET_KEY"], # hm_sk_..., your SECRET key, server side only
base_url="https://your-hermesi-host",
)
result = hermesi.events.trigger(
"order.shipped", # <noun>.<past-tense-verb>
"user_8821", # a subscriber's external_id
{"order_id": "4821", "tracking_url": url},
idempotency_key=f"order-{order.id}-shipped", # see below
)
print(result.event_id, result.status) # evt_..., accepted
202 means the event is recorded and queued. Nothing has been delivered yet: watch the Activity Log in the dashboard.
recipient is a subscriber's external_id, a Subscriber(...) described inline (created or updated on the fly), or a list of up
to 100 of either:
from hermesi import Subscriber
hermesi.events.trigger("order.shipped", Subscriber("cust_331", email="a@example.cm", locale="fr"), {"order_id": "4821"})
The client reads HERMESI_SECRET_KEY and HERMESI_BASE_URL from the environment if you do not pass them.
Idempotency: pass your own key when your code can run twice
The SDK generates a key per call and reuses it across its own retries, which covers a lost response. It cannot cover your
code running twice for the same thing (a webhook handler that is retried, a queue consumer that redelivers): the second run
generates a new key. For that, give the event a key derived from what happened, such as order-4821-shipped. Replaying a key
with the same body within 24 hours returns the original answer, and result.replayed is True.
Other fields
actor=Actor(external_id=..., name=...), delay="15m", send_at=datetime(..., tzinfo=timezone.utc) (a naive datetime is
refused as ambiguous), override={...}, tenant="...". A payload may hold datetime, date, Decimal and UUID values; they
are serialised as ISO text and strings.
Subscriber tokens
A browser or an app talks to Hermesi's client API as one subscriber, with a token minted on your server:
token = hermesi.tokens.mint("user_8821", environment_id="env_01...") # valid for an hour at most
# hand it to your frontend, which sends it with the public key
environment_id is the env_... id shown in the dashboard; the key itself does not carry it. Minting makes no request. The
function is also available on its own: from hermesi import mint_subscriber_token.
Preference links
link = hermesi.subscribers.preference_link("user_8821")
link.url # a hosted page that needs no login and works for about a year
Errors
from hermesi import HermesiAPIError, HermesiConnectionError, NotFoundError, RateLimitError
try:
hermesi.events.trigger("order.shipped", "user_8821", {...})
except NotFoundError:
... # that recipient is not a subscriber in this environment
except RateLimitError as e:
e.retry_after # seconds the server asked to wait (it was too long to wait out)
except HermesiAPIError as e:
e.code, e.request_id # branch on `code`; quote `request_id` to whoever runs Hermesi
except HermesiConnectionError:
... # could not reach Hermesi, after the retries
AuthenticationError (401), ForbiddenError (403), NotFoundError (404), ValidationError (400, 422; see .detail),
RateLimitError (429) and ServerError (5xx) all derive from HermesiAPIError, which derives from HermesiError. Branch on the
class or on code, never on message. A response that is not the API's error envelope has type == "sdk_error" and
code == "unexpected_response", which the API never sends.
Retries
RetryPolicy(max_retries=3, backoff_base=0.5, backoff_cap=8.0, max_retry_after=30.0):
from hermesi import RetryPolicy
Hermesi(..., retry=RetryPolicy(max_retries=0)) # no retrying
A Retry-After is waited out exactly, up to max_retry_after seconds; a server that asks for more gets its error raised, because
a request handler that sleeps for ten minutes is worse than one that fails.
Async
from hermesi import AsyncHermesi
async with AsyncHermesi(api_key=..., base_url=...) as hermesi:
await hermesi.events.trigger("order.shipped", "user_8821", {"order_id": "4821"})
Testing your code
hermesi = Hermesi(simulate=True) # no key, no URL, no network
hermesi.events.trigger("order.shipped", "user_1", {"order_id": "4821"})
assert hermesi.simulated[0].name == "order.shipped"
assert hermesi.simulated[0].payload == {"order_id": "4821"}
A simulated call validates and serialises exactly as a real one does, so a payload that would fail in production fails in your test. It does not know your workflows: it cannot tell you whether an event matches one.
Not included
There is no method for messages or subscribers beyond the preference link, because Hermesi's secret-key API does not have them yet: those are the dashboard's (Management) API. Outbound webhooks are not implemented in Hermesi yet either, so there is nothing to verify.
Development
pip install -e ".[dev]"
pytest
mypy
ruff check .
tests/test_live.py runs the SDK against a real Hermesi and is skipped unless you point it at one; read its docstring.
License
MIT
Metadata
Release files for hermesi 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| hermesi-0.1.0.tar.gz | 53.2 kB | Without provenance | Details for hermesi-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| hermesi-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for hermesi-0.1.0-py3-none-any.whl |
Total release size: 68.9 kB
Release files / hermesi-0.1.0.tar.gz
| Download URL | hermesi-0.1.0.tar.gz |
|---|---|
| Size | 53.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0d2029a598b12c74b53f2e0f021a792a41914b504682a911e6f0ac4b3cce3f41
|
|
BLAKE2b-256 checksum How to use checksums |
9a6e843f073163418abda5beb74248c68affb3d81a154c977f9f436db05848f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|
Release files / hermesi-0.1.0-py3-none-any.whl
| Download URL | hermesi-0.1.0-py3-none-any.whl |
|---|---|
| Size | 15.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fde6a5beed7a9f034c6bf330604c5f6a756905adf7781e10790c6f4ece758e12
|
|
BLAKE2b-256 checksum How to use checksums |
d4c59130a42863592a42559e6088a7b722718a447bff96d6919e619e3018a6fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.0
|
Security for hermesi 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.