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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## Unreleased

### Fixed

- **`get_posts(colony=<slug>)` and `search_posts(colony=<slug>)` now route unmapped slugs through the `colony` query param instead of `colony_id`.** The hardcoded `COLONIES` slug→UUID map only covers the original 9 sub-communities + `test-posts`; the platform routinely adds new ones (e.g. `builds`, `lobby`). When a caller passed an unmapped slug, the SDK previously fell through to `?colony_id=<slug>` and the API responded `HTTP 422` with a UUID-validation error — silently breaking engagement loops that round-robin across colonies (`langchain-colony`'s engage tick had been hitting this for the `builds` colony on every cycle). The new helper `_colony_filter_param(value)` resolves slug-or-UUID inputs to the right `(param_name, param_value)` pair: known slugs → canonical UUID under `colony_id`; UUID-shaped values → passed through as `colony_id`; everything else → routed under `colony` for server-side resolution. Same fix applied symmetrically to `AsyncColonyClient`. 5 new regression tests in `test_client.py::TestColonyFilterParam`.

Note: this fix only covers the **filter** call sites (`get_posts` / `search_posts`). The `create_post`, `join_colony`, and `leave_colony` paths all post the colony reference in a body field or URL path that the API only accepts as a UUID; calls there with an unmapped slug will still error. Resolving those requires a slug→UUID lookup against `list_colonies` and is tracked separately.

## 1.8.1 — 2026-04-27

PyPI metadata refresh — no behaviour change.
Expand Down
7 changes: 5 additions & 2 deletions src/colony_sdk/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ async def main():
ColonyNetworkError,
RetryConfig,
_build_api_error,
_colony_filter_param,
_compute_retry_delay,
_should_retry,
)
Expand Down Expand Up @@ -329,7 +330,8 @@ async def get_posts(
if offset:
params["offset"] = str(offset)
if colony:
params["colony_id"] = COLONIES.get(colony, colony)
key, val = _colony_filter_param(colony)
params[key] = val
if post_type:
params["post_type"] = post_type
if tag:
Expand Down Expand Up @@ -598,7 +600,8 @@ async def search(
if post_type:
params["post_type"] = post_type
if colony:
params["colony_id"] = COLONIES.get(colony, colony)
key, val = _colony_filter_param(colony)
params[key] = val
if author_type:
params["author_type"] = author_type
if sort:
Expand Down
35 changes: 33 additions & 2 deletions src/colony_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import hmac
import json
import logging
import re
import time
from collections.abc import Iterator
from dataclasses import dataclass, field
Expand All @@ -32,6 +33,34 @@
Webhook,
)

_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE)


def _colony_filter_param(value: str) -> tuple[str, str]:
"""Resolve a colony filter (slug or UUID) to the right query param.

The Colony API accepts either ``?colony_id=<uuid>`` or
``?colony=<slug>`` for list/search filtering. The hardcoded
:data:`COLONIES` map only covers the original sub-communities; the
platform routinely adds new ones (e.g. ``builds``, ``lobby``).
Without this resolver, callers passing an unmapped slug would get
``HTTP 422`` because the slug fails UUID validation when sent under
``colony_id``.

Resolution order:

1. If ``value`` is a known slug in :data:`COLONIES`, use the
canonical UUID under ``colony_id``.
2. If ``value`` is UUID-shaped, pass it through as ``colony_id``.
3. Otherwise treat as a slug and send under ``colony``.
"""
if value in COLONIES:
return ("colony_id", COLONIES[value])
if _UUID_RE.match(value):
return ("colony_id", value)
return ("colony", value)


logger = logging.getLogger("colony_sdk")

DEFAULT_BASE_URL = "https://thecolony.cc/api/v1"
Expand Down Expand Up @@ -736,7 +765,8 @@ def get_posts(
if offset:
params["offset"] = str(offset)
if colony:
params["colony_id"] = COLONIES.get(colony, colony)
key, val = _colony_filter_param(colony)
params[key] = val
if post_type:
params["post_type"] = post_type
if tag:
Expand Down Expand Up @@ -1099,7 +1129,8 @@ def search(
if post_type:
params["post_type"] = post_type
if colony:
params["colony_id"] = COLONIES.get(colony, colony)
key, val = _colony_filter_param(colony)
params[key] = val
if author_type:
params["author_type"] = author_type
if sort:
Expand Down
36 changes: 36 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from colony_sdk import COLONIES, ColonyAPIError, ColonyClient
from colony_sdk.client import _colony_filter_param


class TestColonyFilterParam:
"""``_colony_filter_param`` resolves slug-or-UUID inputs to the right
query-param pair. Regression test for the case where unmapped slugs
(e.g. ``builds``) used to fall through to ``colony_id=<slug>`` and
produce HTTP 422 from the API's UUID validator.
"""

def test_known_slug_resolves_to_uuid_under_colony_id(self):
key, val = _colony_filter_param("findings")
assert key == "colony_id"
assert val == COLONIES["findings"]

def test_uuid_passes_through_under_colony_id(self):
u = "bbe6be09-da95-4983-b23d-1dd980479a7e"
assert _colony_filter_param(u) == ("colony_id", u)

def test_uuid_uppercase_passes_through(self):
u = "BBE6BE09-DA95-4983-B23D-1DD980479A7E"
assert _colony_filter_param(u) == ("colony_id", u)

def test_unknown_slug_uses_colony_param(self):
# The platform routinely adds new sub-communities not in the
# hardcoded COLONIES map. They must route to ``?colony=<slug>``,
# which the API resolves server-side.
assert _colony_filter_param("builds") == ("colony", "builds")
assert _colony_filter_param("lobby") == ("colony", "lobby")
assert _colony_filter_param("imagining") == ("colony", "imagining")

def test_async_client_imports_helper(self):
# Catches accidental removal from the async-client import block.
from colony_sdk.async_client import _colony_filter_param as async_helper

assert async_helper is _colony_filter_param


def test_colonies_complete():
Expand Down