# 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) ```python 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("{{ base }}", "USERNAME", "PASSWORD") api.login() print(api.post_rant("Hello from Python", "python,devrant")) ``` ## JavaScript client (drop-in) ```javascript 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("{{ base }}", "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. ```bash # 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={{ base }} python examples/devrant/smoke_test.py DEVRANT_BASE={{ base }} node examples/devrant/smoke_test.mjs ```