lonelypsc
PROJECT STAGE - PRE-ALPHA
This project is in the development stage 2 - pre-alpha. This means that
the project is still in the early stages of development, and is not yet
stable. The current primary focus of the library is building the test
suite in lonelypst
Overview
This is the client library for lonelypss. For more details on when and why you would use lonelyps, as well as the terminology, see the server repository.
Installation
uv sync --extra standard
Usage
In order not to maintain any active connections, you must have a listen socket accepting incoming HTTP(s) requests. This is the main benefit of this library, however, for those occasions where a topic is highly active or for very temporary subscriptions, you can also use a websocket connection which will behave more like what you may be used to from e.g. Redis.
To avoid security mishaps, it's strongly recommended that the endpoints required for the httppubsub subscribers are on a port which only accepts internal traffic.
This client exposes a FastAPI APIRouter that you can bind however you want and a default flow that uses uvicorn to run an ASGI app based on that router.
from lonelypsc.http_client import HttpPubSubClient
from lonelypsc.config.config import HttpPubSubConfig, make_http_pub_sub_config
from lonelypsc.config.auth_config import AuthConfigFromParts
from lonelypsc.config.file_config import get_auth_config_from_file
import json
import os
def _build_config() -> HttpPubSubConfig:
# subscriber-secrets.json is produced from the --setup command on the
# server. generally, this configures HMAC signing authorization between the
# subscribers and broadcasters; if you are using HTTPS you can use token
# authorization instead, or if you have some other way to authorize the
# connections (e.g., TLS mutual auth), or you are sufficiently satisfied
# network communication is internal only, this can setup "none"
# authorization. no matter what, the broadcasters and subscribers will need
# to agree.
incoming_auth_config, outgoing_auth_config = get_auth_config_from_file(
"subscriber-secrets.json"
)
return make_http_pub_sub_config(
# configures how uvicorn is going to bind the listen socket
bind={"type": "uvicorn", "host": "0.0.0.0", "port": 3002},
# configures how the broadcaster is going to connect to us. This can include
# a path, if you are prefixing our router with something, and it can include
# a fragment, which will be used on all subscribe urls.
# ex: you are serving the router's `/v1/receive` at `/pubsub/v1/receive`
# and you are hosting multiple processes on this machine, and this has the
# unique process id of 1, then you might use:
# host="http://192.0.2.0:3002/pubsub#1"
host="http://127.0.0.1:3002",
# the broadcasters that we will try to connect to. note that broadcasters
# are generally stateless, so there is no data loss if one goes down. hence,
# a high-availability setup typically needs only 2 broadcasters to tolerate
# 1 AZ failure.
broadcasters=[
{"host": "http://127.0.0.1:3003"},
],
# how many attempts per broadcaster before giving up; if this is 2,
# for example, we will try every broadcaster once, then we will try
# them all one more time before giving up. will retry 502, 503, and 504
# responses by default, plus network errors if outgoing_retry_network_errors
# is True.
outgoing_retries_per_broadcaster=2,
# how often, in seconds, to check and repair broadcaster subscriptions
resubscribe_interval=300,
# if receiving a message thats larger than this in bytes, it will be spooled to disk
message_body_spool_size=1024 * 1024 * 10,
# total timeout for a request to a broadcaster in seconds
outgoing_http_timeout_total=30,
# total timeout for connecting to a broadcaster in seconds
outgoing_http_timeout_connect=None,
# timeout for a single socket read from a broadcaster in seconds
outgoing_http_timeout_sock_read=None,
# timeout for a single socket connect to a broadcaster in seconds
outgoing_http_timeout_sock_connect=5,
# if we assume the broadcaster did not receive the message where ambiguous
outgoing_retry_ambiguous=True,
auth=AuthConfigFromParts(
# determines how we validate the authorization header when receiving from the broadcaster
incoming_auth_config,
# determines how we set the authorization header when reaching out to the broadcaster
outgoing_auth_config,
),
)
async def main():
async with HttpPubSubClient(_build_config()) as client:
# entering/exiting the client involves setting up/tearing down a server socket, so
# you should generally only have 1 at a time and avoid unnecessarily recreating them
print('Subscribing to foo/bar (exact match) until 1 message is received...')
async with client.subscribe_exact(b'foo/bar') as subscription:
async for message in await subscription.messages():
print(f'Received message on {message.topic}: {message.data.read().decode('utf-8')}')
break
print('Subscribing to multiple topics using glob pattern until 1 message is received...')
async with client.subscribe_glob('foo/*') as subscription:
async for message in await subscription.messages():
print(f'Received message on {message.topic}: {message.data.read().decode('utf-8')}')
break
print('Subscribing to a variety of topics until one message is received...')
async with client.subscribe(exact=[b'foo/bar'], glob=['baz/*']) as subscription:
async for message in await subscription.messages():
print(f'Received message on {message.topic}: {message.data.read().decode('utf-8')}')
break
print('Subscribing to a variety of topics (alt syntax) until one message is received...')
async with client.subscribe_multi() as subscription:
await subscription.subscribe_exact(b'foo/bar')
await subscription.subscribe_glob('baz/*')
async for message in await subscription.messages():
print(f'Received message on {message.topic}: {message.data.read().decode('utf-8')}')
break
print(
'Subscribing to one exact topic until a message is received, '
'with arbitrary timeout behavior...'
)
async with client.subscribe_exact(b'foo/bar') as subscription:
# implementation note: will error if you try to call messages() more than
# once on a subscription
sub_iter = await subscription.messages()
try:
message = await asyncio.wait_for(sub_iter.__anext__(), timeout=5)
except asyncio.TimeoutError:
print('Timed out waiting for message')
else:
print(f'Received message on {message.topic}: {message.data.read().decode('utf-8')}')
print('Subscribing to one exact topic with simple timeout behavior...')
async with client.subscribe_exact(b'foo/bar') as subscription:
async for message in await subscription.with_timeout(5):
if message is None:
print('Been 5 seconds without a message! Ending early')
break
print(f'Received message on {message.topic}: {message.data.read().decode('utf-8')}')
print('Sending a notification to foo/baz with bytes...')
result = await client.notify(topic=b'foo/baz', data=b'Hello, world!')
print(f'Notified {result.notified} subscribers to foo/baz')
print('Sending a notification to foo/baz with a file...')
if not os.path.exists('hello.txt'):
with open('hello.txt', 'w') as f:
f.write('Hello, world!')
with open('hello.txt', 'rb') as f:
result = await client.notify(topic=b'foo/baz', sync_file=f)
print(f'Notified {result.notified} subscribers to foo/baz')
Duplicate messages
With sane usage, i.e., no overlapping glob patterns within a client, duplicate messages are very unlikely. If using overlapping glob patterns (e.g., subscribing to foo/* and foo/baz), duplicate messages are expected and will behave unintuitively. Generally, you should:
- be resilient to duplicated messages
- avoid glob patterns that overlap with each other or with exact topics within the same client
In practice, exact subscriptions handle the vast majority of cases this library
is suitable for, with glob patterns primarily being for analytics/logging/debugging
on a separate client, so this is a relatively non-issue. Generally, just put the
analytics/logging/debugging that attaches to a glob pattern (e.g., ** for everything)
on their own client (e.g via a websocket client).
Notify-Only Clients
If you want to use this library to notify over http but don't need to bind, just set your bind config to a no-op, e.g.,
from fastapi import APIRouter
from lonelypsc.config.config import make_http_pub_sub_config
async def _noop_callback(router: APIRouter) -> None: ...
make_http_pub_sub_config(
bind={"type": "manual", "callback": _noop_callback},
# ... other args omitted for brevity ...
)
Metadata
Release files for lonelypsc 0.0.18
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 |
|---|---|---|---|---|
| lonelypsc-0.0.18.tar.gz | 104.7 kB | With provenance | Details for lonelypsc-0.0.18.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| lonelypsc-0.0.18-py3-none-any.whl | Python 3 | none | any | Details for lonelypsc-0.0.18-py3-none-any.whl |
Total release size: 236.1 kB
Release files / lonelypsc-0.0.18.tar.gz
| Download URL | lonelypsc-0.0.18.tar.gz |
|---|---|
| Size | 104.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
053fcefc523b4e429d0b3fd87ca1d5b22d8089ec360e6e352f6a1ce58f6ff285
|
|
BLAKE2b-256 checksum How to use checksums |
72e13323862442002d472ee53b46e14b5a5ae828cee2025ae5916100b709169f
|
| 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 Sep 22, 2026.
Transparency logRelease files / lonelypsc-0.0.18-py3-none-any.whl
| Download URL | lonelypsc-0.0.18-py3-none-any.whl |
|---|---|
| Size | 131.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4ea442b9e00a5566a0372e4bc354d971095111fef59ec2d1783dd670af0c7cda
|
|
BLAKE2b-256 checksum How to use checksums |
0aaffd52a86374d08ac5acc626c73c437af6fba87cb519bd3390e49f1cee5826
|
| 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 Sep 22, 2026.
Transparency logSecurity for lonelypsc 0.0.18
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for lonelypsc-0.0.18.tar.gz
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 Sep 22, 2026.
Transparency logA 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.