Python
A requests-based client with retries, idempotent submits and a wait helper. Works in a script, a Django or FastAPI service, or a notebook.
The client
import os
import random
import time
from typing import Any, Optional
import requests
FINAL = {"done", "rejected", "failed", "cancelled"}
class SilvertextError(Exception):
def __init__(self, status: int, code: str, message: str):
super().__init__(message)
self.status = status
self.code = code
class Silvertext:
def __init__(self, key: Optional[str] = None, base: str = "https://api.silvertext.com"):
self.key = key or os.environ["SILVERTEXT_API_KEY"]
self.base = base
self.session = requests.Session()
self.session.headers["Authorization"] = f"Bearer {self.key}"
def request(self, method: str, path: str, attempts: int = 4, **kwargs) -> Any:
delay = 1.0
for attempt in range(1, attempts + 1):
try:
res = self.session.request(method, self.base + path, timeout=60, **kwargs)
except requests.RequestException:
if attempt >= attempts:
raise
res = None
if res is not None and res.ok:
return res.json() if res.content else None
retryable = res is None or res.status_code == 429 or res.status_code >= 500
if not retryable or attempt >= attempts:
body = res.json() if res is not None and res.content else {}
err = body.get("error", {})
raise SilvertextError(res.status_code if res is not None else 0, err.get("code", "network"), err.get("message", "request failed"))
retry_after = float(res.headers.get("Retry-After", 0)) if res is not None else 0
time.sleep(retry_after if retry_after > 0 else delay + random.random() * 0.5)
delay = min(delay * 2, 30)
def create_check(self, body: dict, idempotency_key: Optional[str] = None) -> str:
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
return self.request("POST", "/v1/checks", json=body, headers=headers)["id"]
def create_check_from_file(self, path: str, options: Optional[dict] = None, title: Optional[str] = None) -> str:
import json
with open(path, "rb") as f:
data = {"options": json.dumps(options or {})}
if title:
data["title"] = title
return self.request("POST", "/v1/checks", files={"file": (os.path.basename(path), f)}, data=data)["id"]
def get_check(self, check_id: str, view: Optional[str] = None, include: Optional[list] = None) -> dict:
params = {"include": ",".join(include)} if include else ({"view": view} if view else {})
return self.request("GET", f"/v1/checks/{check_id}", params=params)
def cancel_check(self, check_id: str) -> None:
self.request("POST", f"/v1/checks/{check_id}/cancel")
def delete_check(self, check_id: str) -> None:
self.request("DELETE", f"/v1/checks/{check_id}")
def usage(self) -> dict:
return self.request("GET", "/v1/me/usage")
def wait_for_check(self, check_id: str, deadline_s: float = 600) -> dict:
started = time.monotonic()
delay = 3.0
while True:
check = self.get_check(check_id, view="summary")
if check["status"] in FINAL:
return check
if time.monotonic() - started > deadline_s:
try:
self.cancel_check(check_id)
finally:
raise TimeoutError(f"check {check_id} did not finish in time")
time.sleep(delay)
if time.monotonic() - started > 60:
delay = min(delay + 2, 10)Using it
from silvertext import Silvertext
st = Silvertext()
check_id = st.create_check(
{"title": "Q3 launch post", "text": draft, "options": {"citations": False}},
idempotency_key=f"post:{post_id}:v{version}",
)
summary = st.wait_for_check(check_id)
if summary["status"] != "done":
raise RuntimeError((summary.get("rejection") or {}).get("message", summary["status"]))
report = st.get_check(check_id, include=["issues", "sources"])["report"]
grammar = [i for i in report["issues"] if i["category"] == "grammar"]
# A file, as a multipart upload
file_id = st.create_check_from_file("thesis-draft.docx", options={"plagiarism": True, "aiContent": True})Applying fixes to the text
Offsets in issue["original"] index the checked text. Apply replacements from the end backwards so earlier offsets stay valid:
def apply_fixes(text: str, issues: list) -> str:
fixable = [i for i in issues if i.get("fix") and "replacement" in i["fix"]]
for issue in sorted(fixable, key=lambda i: i["original"]["start"], reverse=True):
s, e = issue["original"]["start"], issue["original"]["end"]
text = text[:s] + issue["fix"]["replacement"] + text[e:]
return text