devRant API: Client scripts

Ready-to-run clients for the devRant-compatible protocol. The Python client uses only the
standard library; the JavaScript client uses Node 18+ (global fetch). The full versions, plus
example scripts and an end-to-end conformance test, ship in the repository under
examples/devrant/.

Python client (drop-in)

import json, urllib.parse, urllib.request


class DevRant:
    def __init__(self, base_url, username=None, password=None):
        self.base_url = base_url.rstrip("/")
        self.username, self.password = username, password
        self.auth = {}

    def _request(self, method, path, params=None, body=None):
        merged = dict(params or {}); merged.update(self.auth)
        url = f"{self.base_url}/api/{path.lstrip('/')}"
        data, headers = None, {"Accept": "application/json"}
        if method in ("GET", "DELETE"):
            if merged:
                url += "?" + urllib.parse.urlencode(merged)
        else:
            payload = dict(merged); payload.update(body or {})
            data = urllib.parse.urlencode(payload).encode()
            headers["Content-Type"] = "application/x-www-form-urlencoded"
        req = urllib.request.Request(url, data=data, headers=headers, method=method)
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read().decode())

    def login(self):
        out = self._request("POST", "users/auth-token",
                            body={"username": self.username, "password": self.password})
        if not out.get("success"):
            raise RuntimeError(out.get("error", "login failed"))
        t = out["auth_token"]
        self.auth = {"user_id": t["user_id"], "token_id": t["id"], "token_key": t["key"]}
        return self.auth

    def rants(self, sort="recent", limit=20, skip=0):
        return self._request("GET", "devrant/rants",
                            {"sort": sort, "limit": limit, "skip": skip}).get("rants", [])

    def post_rant(self, text, tags=""):
        return self._request("POST", "devrant/rants", body={"rant": text, "tags": tags})

    def vote_rant(self, rant_id, vote):
        return self._request("POST", f"devrant/rants/{rant_id}/vote", body={"vote": vote})

    def comment(self, rant_id, text):
        return self._request("POST", f"devrant/rants/{rant_id}/comments", body={"comment": text})


api = DevRant("https://devplace.net", "USERNAME", "PASSWORD")
api.login()
print(api.post_rant("Hello from Python", "python,devrant"))

JavaScript client (drop-in)

export class DevRant {
  constructor(baseUrl, username, password) {
    this.baseUrl = baseUrl.replace(/\/$/, "");
    this.username = username; this.password = password; this.auth = {};
  }
  async _request(method, path, params = {}, body = null) {
    const merged = { ...params, ...this.auth };
    const headers = { Accept: "application/json" };
    let url = new URL(`${this.baseUrl}/api/${path.replace(/^\//, "")}`);
    const init = { method, headers };
    if (method === "GET" || method === "DELETE") {
      for (const [k, v] of Object.entries(merged)) url.searchParams.set(k, v);
    } else {
      headers["Content-Type"] = "application/x-www-form-urlencoded";
      init.body = new URLSearchParams({ ...merged, ...(body || {}) }).toString();
    }
    return (await fetch(url, init)).json();
  }
  async login() {
    const out = await this._request("POST", "users/auth-token", {},
      { username: this.username, password: this.password });
    if (!out.success) throw new Error(out.error || "login failed");
    const t = out.auth_token;
    this.auth = { user_id: t.user_id, token_id: t.id, token_key: t.key };
    return this.auth;
  }
  async rants(sort = "recent", limit = 20, skip = 0) {
    return (await this._request("GET", "devrant/rants", { sort, limit, skip })).rants || [];
  }
  postRant(text, tags = "") {
    return this._request("POST", "devrant/rants", {}, { rant: text, tags });
  }
  voteRant(rantId, vote) {
    return this._request("POST", `devrant/rants/${rantId}/vote`, {}, { vote });
  }
  comment(rantId, text) {
    return this._request("POST", `devrant/rants/${rantId}/comments`, {}, { comment: text });
  }
}

const api = new DevRant("https://devplace.net", "USERNAME", "PASSWORD");
await api.login();
console.log(await api.postRant("Hello from Node", "javascript,devrant"));

Example scripts in the repository

examples/devrant/ contains the complete clients and runnable scripts:

File Language What it does
client.py / client.mjs Python / JS Full reusable client (every endpoint).
post_rant.py / post_rant.mjs Python / JS Post one rant from the command line.
feed_watch.py / feed_watch.mjs Python / JS Live feed ticker, optional keyword auto-upvote.
smoke_test.py / smoke_test.mjs Python / JS End-to-end conformance test, prints PASS/FAIL.

They read DEVRANT_BASE, DEVRANT_USERNAME, and DEVRANT_PASSWORD from the environment.

# post a rant
DEVRANT_USERNAME=you DEVRANT_PASSWORD=secret6 \
  python examples/devrant/post_rant.py "Posted from a script" "python"

# run the full conformance test against a running server
DEVRANT_BASE=https://devplace.net python examples/devrant/smoke_test.py
DEVRANT_BASE=https://devplace.net node examples/devrant/smoke_test.mjs