This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 1.2.2 instead.
Reason given by maintainers: No executionStatus and miscounts rejected events. Use the latest version.
aforo-ws-metering
Meter WebSocket traffic — connection duration, message counts, and bytes — by wrapping a connection from the websockets library or a FastAPI/Starlette WebSocket route. One open + one close event per connection by default, or one event per frame.
Version: 1.0.0 · Apache-2.0 · Changelog · User guide
Install
Intended public install:
pip install aforo-ws-metering # core
pip install "aforo-ws-metering[websockets]" # `websockets` library
pip install "aforo-ws-metering[fastapi]" # FastAPI / Starlette
pip install "aforo-ws-metering[httpx]" # faster HTTP flush than stdlib urllib
Not yet on PyPI — install from source for now:
git clone https://github.com/aforoai/SDKs.git
cd SDKs/aforo-metering-sdks/python-ws # folder holding setup.py
pip install -e .
pip install -e ".[fastapi]" # or [websockets] / [httpx]
The core package has no required dependencies — the integration libraries and HTTP client are optional extras.
Quickstart — websockets library
Best when you serve raw WebSocket connections and want connection-level billing without rewriting the handler.
import os, asyncio, websockets
from aforo_ws_metering import AforoWsBilling, track_websockets_connection
billing = AforoWsBilling(
tenant_id="tenant_acme",
product_id="prod_ws_market_feed",
api_key=os.environ["AFORO_API_KEY"],
ingestor_url="https://api.aforo.ai",
)
async def handler(ws):
customer_id = dict(ws.request_headers).get("x-customer-id")
if not customer_id:
await ws.close(code=4401); return
async with await track_websockets_connection(billing, ws, customer_id):
async for msg in ws:
await ws.send(f"echo: {msg}")
async def main():
async with websockets.serve(handler, "0.0.0.0", 8765):
await asyncio.Future() # run forever
asyncio.run(main())
Quickstart — FastAPI / Starlette
from fastapi import FastAPI, WebSocket
from aforo_ws_metering import AforoWsBilling, track_starlette_websocket
billing = AforoWsBilling(
tenant_id="tenant_acme",
product_id="prod_ws_market_feed",
api_key=os.environ["AFORO_API_KEY"],
ingestor_url="https://api.aforo.ai",
)
app = FastAPI()
@app.websocket("/ws")
async def ws_handler(ws: WebSocket):
await ws.accept()
customer_id = ws.headers.get("x-customer-id")
if not customer_id:
await ws.close(code=4401); return
async with await track_starlette_websocket(billing, ws, customer_id):
while True:
data = await ws.receive_text()
await ws.send_text(f"echo: {data}")
Events POST to https://api.aforo.ai/v1/ingest/batch with X-API-Key: <api_key> and X-Tenant-Id: <tenant_id>. The tracker counts sent/received messages and bytes by wrapping the connection's send/recv, and emits a close event with messageCount, dataBytes, and executionDurationMs when the async with block exits.
⚠ Events are sent to the ingestor's
/v1/ingest/batchpath as{"events": [...]}, at most 1000 events per request (larger buffers are split). Setingestor_urlto the host only — the SDK appends the path.
customer_idis resolved by your handler (the examples readx-customer-id) and passed into the tracker — read it from a header your gateway sets, not a value the client can spoof. The tracker doesn't meter a connection you don't wrap.
Configuration
Constructor arguments for AforoWsBilling(...):
| Option | Type | Default | What it does |
|---|---|---|---|
tenant_id |
str |
— (required) | Aforo tenant; sent as X-Tenant-Id. |
product_id |
str |
— (required) | Product the connections bill against. |
api_key |
str |
— (required) | Aforo API key, sent to the ingestor as X-API-Key. |
ingestor_url |
str |
— (required) | Host; /v1/ingest/batch is appended. |
flush_interval_sec |
float |
3.0 |
Background flush cadence (daemon thread from construction). |
flush_count |
int |
100 |
Buffer size that triggers an immediate flush. |
per_frame_events |
bool |
False |
Emit one event per inbound/outbound frame instead of open+close. |
on_error |
Callable[[Exception], None]? |
logs | Called on permanent batch failure, and with the ingestor's errors[].message when it rejects events. |
product_type |
str |
"WEBSOCKET_API" |
Top-level productType sent on every event (trimmed and upper-cased; values the SDK does not know are passed through). Override per event with a productType key in push({...}) or product_type= on track_websockets_connection / track_starlette_websocket. |
Close-code mapping: WS_CLOSE_REASONS maps standard close codes (1000–1011) to descriptor labels (NORMAL_CLOSURE, ABNORMAL_CLOSURE, POLICY_VIOLATION, …); an exception inside the handler surfaces as INTERNAL_ERROR. Retry is fixed at 3 attempts (1s / 2s backoff between them); 408 and 5xx are retried, 429 waits for Retry-After (capped at 60 s), and any other 4xx is not retried.
Walk me through it
Install → wrap a connection → push frames → confirm the event in Aforo, step by step, is in USER_GUIDE.md.
What this doesn't cover
The tracker meters connections you wrap — an unwrapped route emits nothing. Default mode is open+close (aggregated counts); per_frame_events=True is much higher volume, so price for it. It doesn't enforce connection limits or close idle sockets. Pricing and metric mapping are in the Aforo console. For broker fan-out (MQTT) use aforo-mqtt-metering.
Metadata
Release files for aforo-ws-metering 1.0.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 | |
|---|---|---|---|
| aforo_ws_metering-1.0.0.tar.gz | 18.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aforo_ws_metering-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.3 kB
Release files / aforo_ws_metering-1.0.0.tar.gz
| Download URL | aforo_ws_metering-1.0.0.tar.gz |
|---|---|
| Size | 18.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e9e5b286f4c0bbd6ccf7a38af5eb0f10527f354479e63a684b61ce3c1c25b4f5
|
|
BLAKE2b-256 checksum How to use checksums |
f34ae832765511ed177170aab3b05b302aad946459dbbdd28f7787bb14e051dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / aforo_ws_metering-1.0.0-py3-none-any.whl
| Download URL | aforo_ws_metering-1.0.0-py3-none-any.whl |
|---|---|
| Size | 10.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
abd21293f6286a17547c9bf2a121e2cbf4205b3650bb52781bf3be4c2efaf6e3
|
|
BLAKE2b-256 checksum How to use checksums |
4361b84a6a1f623c85abbfae44ebdcaaa004ad4a1c3acb8be0d730a34a71bad6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log