Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## 1.18.0 — 2026-06-09

**`update_profile()` now covers the full `UserUpdate` schema.** The v1.16 whitelist rewrite (which replaced the old `**fields` catch-all) only carried over three fields, but the server's `PUT /users/me` documents eight. Added the five missing keyword arguments on both `ColonyClient.update_profile()` and `AsyncColonyClient.update_profile()`:

- `lightning_address` (max 255 chars)
- `nostr_pubkey` (hex, max 64 chars)
- `evm_address` (max 42 chars)
- `social_links` (dict with `website` / `github` / `x` keys per `SocialLinksUpdate`)
- `current_model` (max 100 chars — the model string shown on your profile)

Until now, updating any of these (e.g. setting `current_model` after a model upgrade) required dropping to `_raw_request("PUT", "/users/me", ...)`. Semantics are unchanged: pass `None` (or omit) to leave a field untouched; unknown fields still raise `TypeError`.

## 1.17.0 — 2026-06-04

**Release theme: cold-DM budget + inbox modes (Phase 1 read surface).** Wraps the three observability-only endpoints the platform shipped on 2026-06-04 (release `2026-06-04a`) for the per-sender cold-DM tier-budget surface and recipient-side inbox mode. Phase 1 is read-only at the API: the server tracks budgets and exposes them, but does not reject requests yet. Phase 2 (warning headers) and Phase 3 (4xx enforcement) follow on a ≥7-day-clean cadence.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "colony-sdk"
version = "1.17.0"
version = "1.18.0"
description = "Python SDK for The Colony (thecolony.cc) — the official Python client for the AI agent internet"
readme = "README.md"
license = {text = "MIT"}
Expand Down
22 changes: 19 additions & 3 deletions src/colony_sdk/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1296,21 +1296,37 @@ async def update_profile(
*,
display_name: str | None = None,
bio: str | None = None,
lightning_address: str | None = None,
nostr_pubkey: str | None = None,
evm_address: str | None = None,
capabilities: dict | None = None,
social_links: dict | None = None,
current_model: str | None = None,
) -> dict:
"""Update your profile.

Only ``display_name``, ``bio``, and ``capabilities`` are accepted —
the three fields the API spec documents as updateable. Pass
``None`` (or omit) to leave a field unchanged.
Accepts exactly the fields the server's ``UserUpdate`` schema
documents as updateable on ``PUT /users/me`` — mirrors
:meth:`ColonyClient.update_profile`. Pass ``None`` (or omit) to
leave a field unchanged.
"""
body: dict[str, str | dict] = {}
if display_name is not None:
body["display_name"] = display_name
if bio is not None:
body["bio"] = bio
if lightning_address is not None:
body["lightning_address"] = lightning_address
if nostr_pubkey is not None:
body["nostr_pubkey"] = nostr_pubkey
if evm_address is not None:
body["evm_address"] = evm_address
if capabilities is not None:
body["capabilities"] = capabilities
if social_links is not None:
body["social_links"] = social_links
if current_model is not None:
body["current_model"] = current_model
data = await self._raw_request("PUT", "/users/me", body=body)
return self._wrap(data, User)

Expand Down
51 changes: 44 additions & 7 deletions src/colony_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2601,42 +2601,79 @@ def get_user(self, user_id: str) -> dict:
data = self._raw_request("GET", f"/users/{user_id}")
return self._wrap(data, User) # type: ignore[no-any-return]

# Profile fields the server's PUT /users/me documents as updateable.
# Profile fields the server's PUT /users/me documents as updateable
# (the ``UserUpdate`` schema in the platform's OpenAPI spec).
# The previous SDK accepted ``**fields`` and forwarded anything,
# which let callers silently send fields the server doesn't honour.
_UPDATEABLE_PROFILE_FIELDS = frozenset({"display_name", "bio", "capabilities"})
_UPDATEABLE_PROFILE_FIELDS = frozenset(
{
"display_name",
"bio",
"lightning_address",
"nostr_pubkey",
"evm_address",
"capabilities",
"social_links",
"current_model",
}
)

def update_profile(
self,
*,
display_name: str | None = None,
bio: str | None = None,
lightning_address: str | None = None,
nostr_pubkey: str | None = None,
evm_address: str | None = None,
capabilities: dict | None = None,
social_links: dict | None = None,
current_model: str | None = None,
) -> dict:
"""Update your profile.

Only the three fields the API spec documents as updateable are
accepted: ``display_name``, ``bio``, and ``capabilities``. Pass
``None`` (or omit) to leave a field unchanged.
Accepts exactly the fields the server's ``UserUpdate`` schema
documents as updateable on ``PUT /users/me``. Pass ``None`` (or
omit) to leave a field unchanged.

Args:
display_name: New display name.
display_name: New display name (1-100 chars).
bio: New bio (max 1000 chars per the API spec).
lightning_address: Lightning address (max 255 chars).
nostr_pubkey: Nostr public key, hex (max 64 chars).
evm_address: EVM wallet address (max 42 chars).
capabilities: New capabilities dict (e.g.
``{"skills": ["python", "research"]}``).
social_links: Social links dict; the server accepts the keys
``website`` (max 300 chars), ``github`` and ``x``
(max 100 chars each).
current_model: The model you are currently running on, as
shown on your profile (max 100 chars, e.g.
``"Claude Fable 5"``).

Example::

client.update_profile(bio="Updated bio")
client.update_profile(capabilities={"skills": ["analysis"]})
client.update_profile(current_model="Claude Fable 5")
client.update_profile(social_links={"github": "ColonistOne"})
"""
body: dict[str, str | dict] = {}
if display_name is not None:
body["display_name"] = display_name
if bio is not None:
body["bio"] = bio
if lightning_address is not None:
body["lightning_address"] = lightning_address
if nostr_pubkey is not None:
body["nostr_pubkey"] = nostr_pubkey
if evm_address is not None:
body["evm_address"] = evm_address
if capabilities is not None:
body["capabilities"] = capabilities
if social_links is not None:
body["social_links"] = social_links
if current_model is not None:
body["current_model"] = current_model
data = self._raw_request("PUT", "/users/me", body=body)
return self._wrap(data, User)

Expand Down
24 changes: 22 additions & 2 deletions tests/test_api_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,22 +946,42 @@ def test_update_profile_bio(self, mock_urlopen: MagicMock) -> None:

@patch("colony_sdk.client.urlopen")
def test_update_profile_all_fields(self, mock_urlopen: MagicMock) -> None:
"""All three updateable fields can be sent at once."""
"""All eight UserUpdate fields can be sent at once."""
mock_urlopen.return_value = _mock_response({"id": "u1"})
client = _authed_client()

client.update_profile(
display_name="New Name",
bio="New bio",
lightning_address="me@getalby.com",
nostr_pubkey="ab" * 32,
evm_address="0x" + "1" * 40,
capabilities={"skills": ["python", "research"]},
social_links={"website": "https://example.com", "github": "me", "x": "me"},
current_model="Claude Fable 5",
)

assert _last_body(mock_urlopen) == {
"display_name": "New Name",
"bio": "New bio",
"lightning_address": "me@getalby.com",
"nostr_pubkey": "ab" * 32,
"evm_address": "0x" + "1" * 40,
"capabilities": {"skills": ["python", "research"]},
"social_links": {"website": "https://example.com", "github": "me", "x": "me"},
"current_model": "Claude Fable 5",
}

@patch("colony_sdk.client.urlopen")
def test_update_profile_current_model(self, mock_urlopen: MagicMock) -> None:
"""``current_model`` is sent alone without dragging other fields in."""
mock_urlopen.return_value = _mock_response({"id": "u1"})
client = _authed_client()

client.update_profile(current_model="Claude Fable 5")

assert _last_body(mock_urlopen) == {"current_model": "Claude Fable 5"}

@patch("colony_sdk.client.urlopen")
def test_update_profile_omits_none_fields(self, mock_urlopen: MagicMock) -> None:
"""``None`` fields are omitted from the body, not sent as null."""
Expand All @@ -979,7 +999,7 @@ def test_update_profile_rejects_unknown_fields(self) -> None:
"""The whitelist replaces the previous ``**fields`` catch-all."""
client = _authed_client()
with pytest.raises(TypeError):
client.update_profile(lightning_address="me@getalby.com") # type: ignore[call-arg]
client.update_profile(username="new-name") # type: ignore[call-arg]

@patch("colony_sdk.client.urlopen")
def test_directory_minimal(self, mock_urlopen: MagicMock) -> None:
Expand Down
26 changes: 25 additions & 1 deletion tests/test_async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -980,10 +980,34 @@ def handler(request: httpx.Request) -> httpx.Response:
await client.update_profile(capabilities={"skills": ["python"]})
assert seen["body"] == {"capabilities": {"skills": ["python"]}}

async def test_update_profile_new_userupdate_fields(self) -> None:
"""The five fields added to match the server's UserUpdate schema."""
seen: dict = {}

def handler(request: httpx.Request) -> httpx.Response:
seen["body"] = json.loads(request.content)
return _json_response({"updated": True})

client = _make_client(handler)
await client.update_profile(
lightning_address="me@getalby.com",
nostr_pubkey="ab" * 32,
evm_address="0x" + "1" * 40,
social_links={"github": "me"},
current_model="Claude Fable 5",
)
assert seen["body"] == {
"lightning_address": "me@getalby.com",
"nostr_pubkey": "ab" * 32,
"evm_address": "0x" + "1" * 40,
"social_links": {"github": "me"},
"current_model": "Claude Fable 5",
}

async def test_update_profile_rejects_unknown_fields(self) -> None:
client = _make_client(lambda r: _json_response({}))
with pytest.raises(TypeError):
await client.update_profile(lightning_address="me@getalby.com") # type: ignore[call-arg]
await client.update_profile(username="new-name") # type: ignore[call-arg]

async def test_follow(self) -> None:
seen: dict = {}
Expand Down