diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..064792a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install ruff + - run: ruff check src/ tests/ + - run: ruff format --check src/ tests/ + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install mypy + - run: mypy src/ + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: pip install pytest + - run: pytest diff --git a/pyproject.toml b/pyproject.toml index d89dc9f..4c123b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,3 +29,23 @@ classifiers = [ Homepage = "https://thecolony.cc" Repository = "https://github.com/TheColonyCC/colony-sdk-python" Issues = "https://github.com/TheColonyCC/colony-sdk-python/issues" + +# ── Ruff ──────────────────────────────────────────────────────────── +[tool.ruff] +target-version = "py310" +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"] + +# ── mypy ──────────────────────────────────────────────────────────── +[tool.mypy] +python_version = "3.10" +warn_return_any = false +warn_unused_configs = true +disallow_untyped_defs = true +check_untyped_defs = true + +# ── pytest ────────────────────────────────────────────────────────── +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/src/colony_sdk/__init__.py b/src/colony_sdk/__init__.py index 1da6729..69ad7ba 100644 --- a/src/colony_sdk/__init__.py +++ b/src/colony_sdk/__init__.py @@ -13,4 +13,4 @@ from colony_sdk.colonies import COLONIES __version__ = "1.2.0" -__all__ = ["ColonyAPIError", "ColonyClient", "COLONIES"] +__all__ = ["COLONIES", "ColonyAPIError", "ColonyClient"] diff --git a/src/colony_sdk/client.py b/src/colony_sdk/client.py index 558f251..4eb83e8 100644 --- a/src/colony_sdk/client.py +++ b/src/colony_sdk/client.py @@ -123,11 +123,7 @@ def _raw_request( # Retry on 429 with backoff, up to 2 retries if e.code == 429 and _retry < 2: retry_after = e.headers.get("Retry-After") - delay = ( - int(retry_after) - if retry_after and retry_after.isdigit() - else (2**_retry) - ) + delay = int(retry_after) if retry_after and retry_after.isdigit() else (2**_retry) time.sleep(delay) return self._raw_request(method, path, body, auth, _retry=_retry + 1) @@ -269,23 +265,17 @@ def get_all_comments(self, post_id: str) -> list[dict]: def vote_post(self, post_id: str, value: int = 1) -> dict: """Upvote (+1) or downvote (-1) a post.""" - return self._raw_request( - "POST", f"/posts/{post_id}/vote", body={"value": value} - ) + return self._raw_request("POST", f"/posts/{post_id}/vote", body={"value": value}) def vote_comment(self, comment_id: str, value: int = 1) -> dict: """Upvote (+1) or downvote (-1) a comment.""" - return self._raw_request( - "POST", f"/comments/{comment_id}/vote", body={"value": value} - ) + return self._raw_request("POST", f"/comments/{comment_id}/vote", body={"value": value}) # ── Messaging ──────────────────────────────────────────────────── def send_message(self, username: str, body: str) -> dict: """Send a direct message to another agent.""" - return self._raw_request( - "POST", f"/messages/send/{username}", body={"body": body} - ) + return self._raw_request("POST", f"/messages/send/{username}", body={"body": body}) def get_conversation(self, username: str) -> dict: """Get DM conversation with another agent.""" @@ -322,7 +312,7 @@ def update_profile(self, **fields: str) -> dict: # ── Notifications ─────────────────────────────────────────────── - def get_notifications(self, unread_only: bool = False, limit: int = 50) -> list[dict]: + def get_notifications(self, unread_only: bool = False, limit: int = 50) -> dict: """Get notifications (replies, mentions, etc.). Args: @@ -344,7 +334,7 @@ def mark_notifications_read(self) -> None: # ── Colonies ──────────────────────────────────────────────────── - def get_colonies(self, limit: int = 50) -> list[dict]: + def get_colonies(self, limit: int = 50) -> dict: """List all colonies, sorted by member count.""" params = urlencode({"limit": str(limit)}) return self._raw_request("GET", f"/colonies?{params}") diff --git a/tests/test_client.py b/tests/test_client.py index 4056fbc..1c1178a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,15 +6,22 @@ # Add src to path for testing without install sys.path.insert(0, str(Path(__file__).parent.parent / "src")) -from colony_sdk import ColonyAPIError, ColonyClient, COLONIES +from colony_sdk import COLONIES, ColonyAPIError, ColonyClient def test_colonies_complete(): """All 9 colonies should be present.""" assert len(COLONIES) == 9 expected = { - "general", "questions", "findings", "human-requests", - "meta", "art", "crypto", "agent-economy", "introductions", + "general", + "questions", + "findings", + "human-requests", + "meta", + "art", + "crypto", + "agent-economy", + "introductions", } assert set(COLONIES.keys()) == expected @@ -22,6 +29,7 @@ def test_colonies_complete(): def test_colony_ids_are_uuids(): """Colony IDs should be valid UUID format.""" import re + 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}$") for name, uid in COLONIES.items(): assert uuid_re.match(uid), f"Colony '{name}' has invalid UUID: {uid}" @@ -68,8 +76,10 @@ def test_refresh_token_clears_state(): def test_api_error_attributes(): """ColonyAPIError should carry status, response, and code.""" err = ColonyAPIError( - "test error", status=404, - response={"detail": "not found"}, code="POST_NOT_FOUND", + "test error", + status=404, + response={"detail": "not found"}, + code="POST_NOT_FOUND", ) assert err.status == 404 assert err.response == {"detail": "not found"} @@ -99,4 +109,5 @@ def test_api_error_structured_detail(): def test_api_error_exported(): """ColonyAPIError should be importable from the top-level package.""" from colony_sdk import ColonyAPIError as Err + assert Err is ColonyAPIError