simplex-chat-python: split Client from Bot, add request/response API (#6976)

* simplex-chat-python: split Client from Bot, add request/response API

Client is now the base class for SimpleX participants that talk TO
services (monitors, probes, automated participants). Bot extends Client
with server features (address, auto-accept, welcome, commands).

New methods on Client (inherited by Bot):
  connect_to(link)           idempotent contact handshake
  send_and_wait(id, text)    send a message and await the reply
  events()                   async iterator over chat events
  @on_message(contact_id=N)  filter by sender in decorators

BotProfile renamed to Profile (alias kept). New ContactAlreadyExistsError
subclass for cleaner error handling.

* simplex-chat-python: narrow event payload type per @on_event tag

@client.on_event("contactConnected") now types the handler's event
parameter as CEvt.ContactConnected instead of the unnarrowed
CEvt.ChatEvent union — mirroring how @on_message narrows by
content_type.

The 50 overloads are generated by the Haskell codegen into _events.py
(as a Protocol class), so new events stay in sync automatically.
Client.on_event is exposed as a property typed as that Protocol; the
runtime implementation is unchanged.
This commit is contained in:
sh
2026-05-13 16:51:00 +01:00
committed by GitHub
parent 3b4bf92015
commit 9584992c83
10 changed files with 2089 additions and 663 deletions
@@ -40,10 +40,26 @@ def _db_to_migrate_args(db: Db) -> tuple[str, str, _native.Backend]:
class ChatCommandError(Exception):
"""A chat command returned an unexpected response type.
`response` is the raw wire response; `response_type` exposes its `type`
discriminator for quick checks. Subclasses cover known recoverable cases
so callers can `except ContactAlreadyExistsError` instead of inspecting
`response.get("type")` themselves.
"""
def __init__(self, message: str, response: CR.ChatResponse):
super().__init__(message)
self.response = response
@property
def response_type(self) -> str:
return self.response.get("type", "") # type: ignore[return-value]
class ContactAlreadyExistsError(ChatCommandError):
"""`api_connect`/`api_connect_active_user` was called for an existing contact."""
class ChatApi:
def __init__(self, ctrl: int):
@@ -481,7 +497,7 @@ class ChatApi:
if r["type"] == "sentInvitation":
return "contact"
if r["type"] == "contactAlreadyExists":
raise ChatCommandError("contact already exists", r)
raise ContactAlreadyExistsError("contact already exists", r)
raise ChatCommandError("connection error", r)
async def api_accept_contact_request(self, contact_req_id: int) -> T.Contact: