Blog

Keeping six CalmLens SDKs in step

Python and Go now match the JavaScript SDK, Rust, Ruby and Java have guides, and shared fixtures check that every SDK sends the same requests.

6 min read

  • SDKs
  • Testing
  • Webhooks

Release 2026.6 brings the Python and Go SDKs up to the same core operations as the JavaScript SDK, adds guides for Rust, Ruby and Java, and gives every SDK guide a testing page with offline mock examples.

What each SDK covers

The Python and Go SDKs implement the public network operations of the JavaScript SDK: creating and moderating assets, retrieving, updating, listing, moving and retrying them, multipart uploads, signed URLs, folders, the events feed, ensuring lens definitions, building CDN URLs and reading CDN content. Both also verify webhook signatures.

Each SDK follows its language's conventions rather than copying JavaScript's.

SDKRequirementsShape
PythonPython 3.10+, no runtime dependenciesSynchronous snake_case methods; options and responses keep the API's camelCase keys
GoGo 1.22+, standard library onlyMethods take a context.Context and typed option structs
RustRust 1.88+Blocking client
RubyRuby 3.1+snake_case options; responses keep API field names
JavaJava 17+Synchronous client

From async Python code, the guide suggests running a call in a thread with asyncio.to_thread. Python and Go accept file-like objects and io.Reader streams for uploads. Rust, Ruby and Java implement the same request operations, CDN access, lens caching and webhook verification, but they buffer uploads in memory and don't offer async or streaming clients.

Failures behave the same way in every SDK. API errors carry the API's error code and HTTP status. A timeout raises sdk/timeout, and other network or protocol failures raise sdk/transport-error, with the original exception kept as the cause. The Python and Go SDKs never retry uploads or other writes automatically, so a write that fails in transit is not silently repeated.

The JavaScript SDK is published to npm as @calmlens/sdk. The Python, Go, Rust, Ruby and Java SDKs are not yet available from PyPI, the Go module proxy, crates.io, RubyGems or Maven Central. Their guides document the API each package exposes.

The CalmLens CLI is now distributed separately from the SDKs, with standalone downloads and Homebrew installation alongside npm. The Python and Go packages no longer install CLI commands of their own.

Testing without network calls

Every SDK guide now links to a testing page. Each SDK ships a mock client that accepts the real client's configuration and never leaves the process, so tests need no API key, project or network access.

In Python, the mock is a CalmLensClient whose transport is replaced with an in-memory backend. Request building and validation run unchanged, and every request is recorded:

from calmlens.testing import MockCalmLensClient

client = MockCalmLensClient(project_id="project", timeout=5)
assert client.text.moderate(text="Hello")["status"] == "approved"
assert client.requests[-1].path == "/projects/project/assets/upload"

client.enqueue_error(TimeoutError("timed out"))
# The next call raises CalmLensError with code "sdk/timeout".

The Go mock in the testing package works the same way. It embeds the real client, mock.Requests() returns each captured request with its method, path, parsed query and decoded body, and mock.FailNext(err) makes the next request fail with sdk/transport-error, or sdk/timeout for context.DeadlineExceeded.

The JavaScript, Python and Go mocks keep state. An asset you create can be retrieved, updated and listed afterwards, and you can fix IDs, timestamps and the statuses assigned to queued and synchronous uploads so assertions stay reproducible. In JavaScript, these helpers live under @calmlens/sdk/testing, which the main entry point never imports, so they stay out of production bundles:

import { MockCalmLensClient, createMockApiKey } from "@calmlens/sdk/testing";

const client = new MockCalmLensClient({
  apiKey: createMockApiKey(),
  projectId: "123e4567-e89b-12d3-a456-426614174000",
});

client.failNextRequest("timeout", { operation: "text.moderate" });
await expect(client.text.moderate({ text: "hello" })).rejects.toMatchObject({
  code: "sdk/timeout",
});

The Rust, Ruby and Java mocks are programmable instead. You queue the responses your test expects, in order, and inspect the requests the client sent. An empty queue fails without making a network call, so a test that triggers an extra request fails rather than passing by accident.

No mock classifies content. They return configured decisions and don't fetch remote URLs or transform media. Use them to test your own branching on approved, rejected and error codes, and test real moderation results against a project.

One recorded contract for every SDK

Each SDK is written by hand in its own language. The sdk-contract package in the CalmLens repository checks them against each other: it records what the JavaScript SDK sends, and every other SDK's tests must reproduce it.

Its generator runs the real JavaScript client with a fetch backend that captures each request instead of sending it. For a fixed list of operations and options, it writes the method, path, query and body to requests.json, alongside the response the test should return. It also writes webhooks.json, which contains webhook signatures, verification cases with their expected results, and event-to-payload conversions, all produced by the JavaScript SDK.

The Go module keeps the original fixtures so its tests run on their own, and the generator refreshes package-local copies for Rust, Ruby and Java. Each SDK's native test suite replays the fixtures through its own client and fails if the method, route, query or body differs from the JavaScript recording.

The text moderation case submits héllo 🌍, which is 7 characters and 11 bytes in UTF-8. The recorded sizeInBytes is 11, so a client that counted characters instead of bytes would fail it. Webhook verification across SDKs shares one test vector for the v1= HMAC-SHA256 signature over <timestamp>.<raw body>, with a default tolerance of 300 seconds.

When the JavaScript SDK changes on purpose, the generator is run again and the fixtures are committed. Every other SDK then has to match the new requests before its tests pass again.

The contract allows for intentional language differences. Binary input is bytes or streams instead of browser Blob and ReadableStream types. In Python and Go, concurrent requests to ensure the same lens are deduplicated, but general request coalescing from the JavaScript transport is not copied, and the clients don't follow credential-bearing redirects by default.

To choose a language and see its operations, errors and testing page, start at the SDK overview.

Signing in

Signing out

Cookies on CalmLens

We use essential cookies to keep you signed in and the site secure. With your permission, we'll also use functional cookies to remember preferences such as your layout and documentation language, and to offer Google One Tap sign-in. We don't use analytics or advertising cookies. Manage cookies