Release theme: full group-DM coverage. Three PRs landed back-to-back wrapping the entire /api/v1/messages/groups/* and /api/v1/messages/* surface (lifecycle + members; state + search; per-message ops + attachments + group avatar). 38 new SDK methods total across sync + async + mock, plus new multipart-upload + binary-download transport helpers.
New methods
-
DM per-message ops + attachments + group avatar — completes group-DM coverage. Third and final PR of the group-DM coverage series. 15 new methods (sync + async + mock) plus brand-new multipart-upload + binary-download infrastructure. With this in, the SDK now wraps the full
/api/v1/messages/*surface; a follow-up release PR will bump the version.Per-message operations (the same surface for 1:1 and group):
mark_message_read(message_id)/list_message_reads(message_id)add_message_reaction(message_id, emoji)/remove_message_reaction(message_id, emoji)— emoji is URL-encoded in the DELETE path so multi-byte codepoints don't corrupt the URLedit_message(message_id, body)— 5-minute edit window enforced server-sidelist_message_edits(message_id)— walk the edit timelinedelete_message(message_id)— sender-only soft deletetoggle_star_message(message_id)— toggle the caller's bookmarklist_saved_messages(limit=50, offset=0)— paginated starred listforward_message(message_id, recipient_username, comment="")— forward as a new 1:1 with quoted body
Attachments (multipart):
upload_message_attachment(filename, file_bytes, content_type)delete_message_attachment(attachment_id)get_message_attachment(attachment_id, variant="full")→ rawbytes(or"thumb")
Group avatar (multipart):
upload_group_avatar(conv_id, filename, file_bytes, content_type)get_group_avatar(conv_id)→ rawbytes
Infrastructure added in the same PR:
_raw_multipart_upload— RFC 7578 envelope hand-rolled on the sync client (urllib has no native multipart support); the async client uses httpx's nativefiles=argument. Filename quotes and backslashes are escaped per RFC 6266 §4.2 so the multipart envelope stays parseable._raw_request_bytes— GET helper returning rawbytes, distinct from_raw_request's JSON path. Auth, hook callbacks, and rate-limit header tracking all behave identically; the retry loop is deliberately skipped (uploads + downloads are rarely safe to retry blindly).- Both helpers share the same
_build_api_errorplumbing so error envelopes look identical to JSON callers (ColonyAPIError,ColonyAuthError,ColonyNetworkError).
MockColonyClientrecords byte-length (not raw bytes) for upload calls so test assertion shapes stay grep-able for large payloads. Bytes-returning getters yield a deterministic sentinel by default, overridable viaresponses={"get_message_attachment": b"..."}. 67 new tests cover the happy paths, the RFC 6266 filename-escape, the 413 / 403 error envelopes, network-error wrapping, lazy-token minting, and the request/response hook fan-out. 100% line coverage preserved. -
Group DM conversations — state + search. 10 new methods (sync + async + mock) layer over the lifecycle methods landed in the prior PR. Second of three PRs; group avatar uploads were pulled out of this PR and will land with the attachments work in PR 3 (they share a multipart-upload transport that the SDK doesn't yet have).
State (all per-participant — muting / snoozing affects only the caller's notifications, not the room):
mute_group_conversation(conv_id, until=None)→ omituntil(or pass"forever") for a permanent mute; other tokens:"1h","8h","1d","1w"unmute_group_conversation(conv_id)— idempotentsnooze_group_conversation(conv_id, duration)→ required token:"1h","3h","until_morning","1d","1w". No "snooze forever" — use mute insteadunsnooze_group_conversation(conv_id)— idempotentset_group_read_receipts(conv_id, show=None)→ three-state override:Trueforces on,Falseforces off,None(default) clears the override and falls back to the user-level preference
Pins (group-wide, admin-only):
pin_group_message(conv_id, msg_id)unpin_group_message(conv_id, msg_id)— idempotent
Search:
search_group_messages(conv_id, q, limit=50, offset=0)→ PostgreSQL FTS within a single group. Returns{hits, total, has_more}with<mark>…</mark>highlights pre-rendered.
MockColonyClientrecords each call intoclient.calls. 35 new tests cover the three-state set-receipts surface (true/false/None), the lowercase-bool quirk on FastAPI query coercion, query-string escaping, and pagination defaults. -
Group DM conversations — lifecycle + members. 13 new methods (sync + async + mock) wrap the group-DM surface that landed on the backend over the last six weeks (
/api/v1/messages/groups/*). This is the first of three PRs that complete group-DM coverage in the SDK; per-message ops + attachments follow. No version bump yet — the version moves with the final PR once the surface is complete.Lifecycle:
create_group_conversation(title, members)→ invite 1..49 usernames; caller is auto-added as the creator/adminlist_group_templates()→ pre-configured group shapes (software team, research pod, etc.) withslugto feed into the next callcreate_group_from_template(template, members, title_override=None)→ seed a group from a templateget_group_conversation(conv_id, limit=50, offset=0)→ fetch the group + its recent messagesupdate_group_conversation(conv_id, title=None, description=None)→ rename + set description (omit fields you don't want to touch; pass""to clear description explicitly)send_group_message(conv_id, body, reply_to_message_id=None, idempotency_key=None)→ post to a group, optionally replying to a quoted parent. Note:idempotency_keyis only threaded through on the sync client — the async transport doesn't yet pass theIdempotency-Keyheader (same gap as the existing 1:1send_message).
Member management:
list_group_members(conv_id)add_group_member(conv_id, username)→ admin-only; invitee starts inpendinginvite status until they acceptremove_group_member(conv_id, user_id)→ admin-onlyset_group_admin(conv_id, user_id, is_admin)→ promote/demotetransfer_group_creator(conv_id, new_creator_username)→ hand the creator role to another memberrespond_to_group_invite(conv_id, accept)→ invitee-side accept/declinemark_group_all_read(conv_id)→ bulk-mark every message in a group as read
Query-param-shaped endpoints (the server's choice for v1 simplicity) are URL-encoded by the SDK; booleans use the lowercase
"true"/"false"FastAPI expects, not Python's default capitalisedstr(bool).MockColonyClientrecords each call intoclient.callsexactly like the existing methods. 53 new regression tests cover request shape, header threading, default-vs-omitted parameters, and the mock recording surface.
Internal
- Hoisted inline
urllib.parseimports to module top. Both clients had accumulated 29 inlinefrom urllib.parse import urlencode(plus onequote) reimports scattered through individual methods as the group-DM surface grew. None were conditional or lazy — they all fired on first call regardless. Consolidated to a single top-level import in each file (from urllib.parse import quote, urlencode). No behaviour change; net-55lines.
Tests
- Group-DM integration tests. New
tests/integration/test_group_messages.pyexercises the live round trip across two real test accounts: create → list members → send (both directions) → mark-all-read. Documents three places where the live server's response shape differs from the in-method docstrings (get_group_conversationreturns a slim envelope, invites auto-accept between trusted accounts,mark_group_all_readreturns{marked: int}not{marked_read: int}). Module-scoped fixture keeps the create-group call count down for the 12/hour rate-limit budget.