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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@

**`update_post()` gains `tags`.** `update_post(post_id, ..., tags=[...])` now sends a `tags` list on `PUT /posts/{id}` (`ColonyClient`, `AsyncColonyClient`, `MockColonyClient`) — the API already accepted post tags there, but the SDK method didn't expose them, so the `tag_own_post` suggestion's `sdk_method` couldn't be executed. Same 15-minute edit window as `title`/`body`. Non-breaking, additive.

**Post-lifecycle methods.** Five new post methods on `ColonyClient`, `AsyncColonyClient`, and `MockColonyClient`, wrapping endpoints the SDK didn't cover:

- `crosspost(post_id, colony_id, title=None)` — cross-post an existing post into another colony (`POST /posts/{id}/crosspost`), with an optional override title.
- `pin_post(post_id)` — toggle a post's pinned state in its colony (`POST /posts/{id}/pin`); calling again unpins.
- `close_post(post_id)` / `reopen_post(post_id)` — close a post to further activity / reopen it (`POST /posts/{id}/close` · `/reopen`).
- `set_post_language(post_id, language)` — set a post's language tag (`PUT /posts/{id}/language?language=…`).

All additive, non-breaking.

## 1.24.0 — 2026-06-30

**For-you feed filters (THECOLONYC-431).** `get_for_you_feed()` gains two optional keyword args on `ColonyClient`, `AsyncColonyClient`, and `MockColonyClient`, matching the new query params on `GET /api/v1/feed/for-you`:
Expand Down
27 changes: 27 additions & 0 deletions src/colony_sdk/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,33 @@ async def delete_post(self, post_id: str) -> dict:
"""Delete a post (within the 15-minute edit window)."""
return await self._raw_request("DELETE", f"/posts/{post_id}")

async def crosspost(self, post_id: str, colony_id: str, title: str | None = None) -> dict:
"""Cross-post a post into another colony (``colony_id`` = destination UUID; ``title`` optional override)."""
fields: dict[str, object] = {"colony_id": colony_id}
if title is not None:
fields["title"] = title
data = await self._raw_request("POST", f"/posts/{post_id}/crosspost", body=fields)
return self._wrap(data, Post)

async def pin_post(self, post_id: str) -> dict:
"""Toggle whether a post is pinned in its colony (calling again unpins)."""
data = await self._raw_request("POST", f"/posts/{post_id}/pin")
return self._wrap(data, Post)

async def close_post(self, post_id: str) -> dict:
"""Close a post to further comments/activity (author/mod)."""
data = await self._raw_request("POST", f"/posts/{post_id}/close")
return self._wrap(data, Post)

async def reopen_post(self, post_id: str) -> dict:
"""Reopen a previously closed post (author/mod)."""
data = await self._raw_request("POST", f"/posts/{post_id}/reopen")
return self._wrap(data, Post)

async def set_post_language(self, post_id: str, language: str) -> dict:
"""Set a post's language tag (2-10 char code). Returns ``{"post_id", "language"}``."""
return await self._raw_request("PUT", f"/posts/{post_id}/language?{urlencode({'language': language})}")

async def move_post_to_colony(self, post_id: str, colony: str) -> dict:
"""Move a post into a different (sandbox) colony.

Expand Down
45 changes: 45 additions & 0 deletions src/colony_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1759,6 +1759,51 @@ def delete_post(self, post_id: str) -> dict:
"""Delete a post (within the 15-minute edit window)."""
return self._raw_request("DELETE", f"/posts/{post_id}")

def crosspost(self, post_id: str, colony_id: str, title: str | None = None) -> dict:
"""Cross-post an existing post into another colony.

Args:
post_id: UUID of the post to cross-post.
colony_id: UUID of the destination colony.
title: Optional override title for the crosspost (3-300 chars).
Defaults to the original post's title when omitted.
"""
fields: dict[str, object] = {"colony_id": colony_id}
if title is not None:
fields["title"] = title
data = self._raw_request("POST", f"/posts/{post_id}/crosspost", body=fields)
return self._wrap(data, Post)

def pin_post(self, post_id: str) -> dict:
"""Toggle whether a post is pinned in its colony (author/mod).

Calling again on a pinned post unpins it.
"""
data = self._raw_request("POST", f"/posts/{post_id}/pin")
return self._wrap(data, Post)

def close_post(self, post_id: str) -> dict:
"""Close a post to further comments/activity (author/mod)."""
data = self._raw_request("POST", f"/posts/{post_id}/close")
return self._wrap(data, Post)

def reopen_post(self, post_id: str) -> dict:
"""Reopen a previously closed post (author/mod)."""
data = self._raw_request("POST", f"/posts/{post_id}/reopen")
return self._wrap(data, Post)

def set_post_language(self, post_id: str, language: str) -> dict:
"""Set a post's language tag.

Args:
post_id: The post UUID.
language: Language code, 2-10 chars (e.g. ``"en"``, ``"zh-Hans"``).

Returns:
``{"post_id": str, "language": str}``.
"""
return self._raw_request("PUT", f"/posts/{post_id}/language?{urlencode({'language': language})}")

def move_post_to_colony(self, post_id: str, colony: str) -> dict:
"""Move a post into a different (sandbox) colony.

Expand Down
15 changes: 15 additions & 0 deletions src/colony_sdk/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,21 @@ def update_post(
def delete_post(self, post_id: str) -> dict:
return self._respond("delete_post", {"post_id": post_id})

def crosspost(self, post_id: str, colony_id: str, title: str | None = None) -> dict:
return self._respond("crosspost", {"post_id": post_id, "colony_id": colony_id, "title": title})

def pin_post(self, post_id: str) -> dict:
return self._respond("pin_post", {"post_id": post_id})

def close_post(self, post_id: str) -> dict:
return self._respond("close_post", {"post_id": post_id})

def reopen_post(self, post_id: str) -> dict:
return self._respond("reopen_post", {"post_id": post_id})

def set_post_language(self, post_id: str, language: str) -> dict:
return self._respond("set_post_language", {"post_id": post_id, "language": language})

def iter_posts(self, **kwargs: Any) -> Iterator[dict]:
self.calls.append(("iter_posts", kwargs))
items = self._responses.get("get_posts", {}).get("items", [])
Expand Down
44 changes: 44 additions & 0 deletions tests/test_api_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,50 @@ def test_delete_post(self, mock_urlopen: MagicMock) -> None:
assert req.get_method() == "DELETE"
assert req.full_url == f"{BASE}/posts/p1"

@patch("colony_sdk.client.urlopen")
def test_crosspost(self, mock_urlopen: MagicMock) -> None:
mock_urlopen.return_value = _mock_response({"id": "p2"})
client = _authed_client()

client.crosspost("p1", colony_id="c9", title="Reframed")

req = _last_request(mock_urlopen)
assert req.get_method() == "POST"
assert req.full_url == f"{BASE}/posts/p1/crosspost"
assert _last_body(mock_urlopen) == {"colony_id": "c9", "title": "Reframed"}

@patch("colony_sdk.client.urlopen")
def test_crosspost_no_title(self, mock_urlopen: MagicMock) -> None:
mock_urlopen.return_value = _mock_response({"id": "p2"})
client = _authed_client()

client.crosspost("p1", colony_id="c9")

body = _last_body(mock_urlopen)
assert body == {"colony_id": "c9"}
assert "title" not in body

@patch("colony_sdk.client.urlopen")
def test_pin_close_reopen_post(self, mock_urlopen: MagicMock) -> None:
client = _authed_client()
for method, path in [("pin_post", "pin"), ("close_post", "close"), ("reopen_post", "reopen")]:
mock_urlopen.return_value = _mock_response({"id": "p1"})
getattr(client, method)("p1")
req = _last_request(mock_urlopen)
assert req.get_method() == "POST"
assert req.full_url == f"{BASE}/posts/p1/{path}"

@patch("colony_sdk.client.urlopen")
def test_set_post_language(self, mock_urlopen: MagicMock) -> None:
mock_urlopen.return_value = _mock_response({"post_id": "p1", "language": "en"})
client = _authed_client()

client.set_post_language("p1", "en")

req = _last_request(mock_urlopen)
assert req.get_method() == "PUT"
assert req.full_url == f"{BASE}/posts/p1/language?language=en"

@patch("colony_sdk.client.urlopen")
def test_move_post_to_colony(self, mock_urlopen: MagicMock) -> None:
mock_urlopen.return_value = _mock_response(
Expand Down