scribe-client (Python)
Python bindings for the Scribe document conversion API. This package is a thin, synchronous wrapper (via PyO3) around the Rust scribe-client crate, so it behaves the same as the Rust client.
- Synchronous by design. Every method blocks until the request completes; there's no
asyncioAPI. Under the hood each call runs on a shared Tokio runtime with the GIL released, so other Python threads keep running while a request is in flight. - Auto-refreshing tokens.
ScribeClientholds aTokenSetand refreshes it automatically when it's missing or about to expire, and again if a request unexpectedly comes back401.
Installing
pip install scribe-client
Wheels are built for Linux, macOS and Windows, for Python 3.9 and later.
To build it from source instead, with maturin:
cd crates/scribe-client-py
uv venv # or: python -m venv .venv
source .venv/bin/activate
uv pip install maturin
maturin develop # builds the extension and installs it into the venv
Authenticating
The server uses OAuth 2.0 Authorization Code + PKCE. There's no password or client-credentials grant, so a human has to approve access once via a browser; after that, ScribeClient handles refreshing the token for you.
from scribe_client import AuthClient, PkceChallenge, ScribeClient
auth = AuthClient(base_url, client_id)
pkce = PkceChallenge()
# Send the user to this URL. After they log in and approve, they're
# redirected to redirect_uri with a `code` query parameter.
url = auth.authorization_url(redirect_uri, pkce)
# Exchange the code (and the verifier from the *same* PkceChallenge) for a
# token set.
tokens = auth.exchange_code(redirect_uri, code, pkce.verifier)
client = ScribeClient(base_url, client_id, tokens)
tokens.access_token, tokens.refresh_token, and tokens.expires_at (Unix seconds, or None) are readable if you want to persist them between runs — reconstruct a TokenSet later with TokenSet(access_token, refresh_token, expires_at).
AuthClient doesn't open a browser or listen for the redirect; getting the code back is up to your application. The server never shows the code on a page — it only ever redirects to redirect_uri — so something has to be listening there. For a desktop app or script, register a loopback URI with a fixed port, such as http://127.0.0.1:8765/callback, and run a small HTTP server on that address while the user signs in. See the top-level README for other kinds of app.
See examples/pkce_flow.py for a runnable end-to-end version of this, including creating a document and converting it. Given a loopback SCRIBE_REDIRECT_URI, it opens the browser and catches the redirect itself:
export SCRIBE_BASE_URL=https://scribeit.io
export SCRIBE_CLIENT_ID=your-client-id
export SCRIBE_REDIRECT_URI=http://127.0.0.1:8765/callback # as registered for your app
export SCRIBE_DOCUMENT_PATH=report.docx
uv run --project crates/scribe-client-py crates/scribe-client-py/examples/pkce_flow.py
With any other redirect URI, the browser ends up on a page that fails to load after you approve access. That's expected: copy the code parameter from the address bar and paste it into the script. A code works only once and expires after a few minutes.
Documents
# From a local file...
document_id = client.create_document_from_file("report.docx", data)
# ...or have the server fetch it from a URL.
document_id = client.create_document_from_url("https://example.com/report.pdf")
# Both take an optional password, for a file you already know is protected.
document_id = client.create_document_from_file("locked.pdf", data, "hunter2")
Creating a document automatically starts an html_stream conversion (a fast preview), so the caller has something to show immediately even if the account is out of page credits for a full conversion.
result = client.list_documents()
print(result.pages_remaining) # None if the account isn't page-metered
for doc in result.documents:
# doc.title and doc.page_count are None until the server has
# determined them (e.g. briefly, for a URL-sourced document).
print(doc.id, doc.title, doc.page_count, doc.inserted_at)
for output in doc.outputs:
print(" ", output.format, output.stage, output.progress)
client.delete_document(document_id)
list_outputs(document_id) returns an OutputList for one document — .outputs, the same rows list_documents() embeds per-document without the document metadata, plus .is_password_needed; get_settings/update_settings read and partially update a document's conversion settings (language, TTS voice, Braille table, and so on — see Settings's attributes for the full set).
settings = client.get_settings(document_id)
client.update_settings(document_id, {"large_print": True})
update_settings takes a plain dict; only the keys you pass are changed server-side.
Converting to another format
Creating a document starts only the html_stream conversion. Converting to any other format — pdf, epub, daisy, docx, brf, mp3, offline_html, mobi — happens over a real-time channel, not a REST call, so the server can guarantee it's subscribed to progress on whatever it just started converting.
The server runs one conversion of a document at a time, and start_conversion raises ConversionInProgressError while another is running. That includes the html_stream conversion that creating the document started, unless it turned out to be a preview, so a call straight after creating a document will normally raise it. Wait for that conversion's "conversion_complete" event, then call start_conversion again:
channel = client.open_document_channel(document_id)
try:
while True:
try:
output_id = channel.start_conversion("pdf")
break
except ConversionInProgressError:
while True:
event = channel.next_event()
if event["type"] == "conversion_complete":
break
if event["type"] == "error":
raise RuntimeError(event["reason"])
while True:
event = channel.next_event()
if event["type"] == "status":
print(f"{event['format']}: {event['stage']} {event['progress']:.0%}")
elif event["type"] == "conversion_complete" and event["format"] == "pdf":
data = client.download_output(document_id, "pdf")
break
elif event["type"] == "error":
raise RuntimeError(event["reason"])
finally:
channel.close()
Calling start_conversion for a format that's already complete just returns that output's existing id without converting it again. Calling it for a format that's still converting raises ConversionInProgressError, like any other running conversion.
next_event() blocks until the next event arrives and returns it as a dict tagged by event["type"]. It sends heartbeats while it waits, which is what keeps the connection open during a long conversion, so keep calling it rather than doing other long work between calls. The server closes a connection it hasn't heard from for about a minute. The connection can still be lost, for example to a network failure, which raises ScribeApiError. The conversion carries on without it: open a new channel on the same document, and don't upload it again.
type |
other keys | meaning |
|---|---|---|
"status" |
format, stage, progress |
a conversion's stage or progress changed |
"chunk" |
content |
a chunk of streamed HTML (html_stream only) |
"conversion_complete" |
format, output_id |
a format finished converting |
"error" |
reason |
an out-of-band error unrelated to a specific call |
"password_required" |
— | the document is protected and hasn't been unlocked |
stage is one of "queue", "start", "convert", "add_image_descriptions", or "complete".
Joining the channel also delivers events for any formats that were already converting (or complete) when you joined — including incomplete parent formats. (Most formats are rendered from the html output, and daisy is additionally assembled from mp3; the server subscribes you to those automatically when they're not finished yet.) Filter on event["format"] if you only care about one format at a time, as in the example above.
Password-protected documents
A protected PDF can't be converted until its password is supplied. The server reports this in three places: password_required is pushed as soon as you join the channel, start_conversion raises PasswordRequiredError rather than queueing work that can only fail, and is_password_needed is set on the document in list_documents() and list_outputs().
Pass the password to start_conversion to unlock and convert in one call. It's stored against the document, so converting it to further formats later doesn't need it again.
try:
output_id = channel.start_conversion("pdf")
except PasswordRequiredError:
output_id = channel.start_conversion("pdf", prompt_for_password())
A wrong password isn't rejected here — the server accepts it, the converter fails on it, and an "error" event with reason "invalid_password" arrives from next_event(). Prompt again and call start_conversion again. A locked document has no outputs at all, so there's nothing to clean up between attempts.
Always close() the channel when you're done with it (a try/finally is the easiest way, as above) — it holds an open WebSocket connection.
Errors
All exceptions are subclasses of ScribeApiError:
| Exception | Raised when |
|---|---|
NotFoundError |
the document (or channel) doesn't exist |
ForbiddenError |
the document exists but isn't owned by the current user |
ConversionNotCompleteError |
download_output was called before that format finished converting |
ConversionInProgressError |
start_conversion was called while a conversion of the document is running |
RateLimitedError |
start_conversion was called too many times too quickly |
PasswordRequiredError |
the document is a protected file and no password has been supplied |
NeedsPurchaseError |
the account doesn't have enough page credits for the requested conversion |
InvalidGrantError |
an OAuth code, refresh token, or PKCE verifier didn't check out |
ScribeApiError (base) |
anything else the server rejected, or a connection-level failure |
from scribe_client import NotFoundError
try:
client.delete_document(document_id)
except NotFoundError:
print("already gone")
Running the tests
cd crates/scribe-client-py
maturin develop
pytest
Tests don't hit a real server: tests/conftest.py runs a small dependency-free HTTP server (stdlib http.server) for REST calls, and a hand-rolled WebSocket server (stdlib socket, doing the RFC 6455 handshake itself) for exercising DocumentChannel.
Metadata
Release files for scribe-client 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| scribe_client-0.2.0.tar.gz | 89.1 kB | With provenance | Details for scribe_client-0.2.0.tar.gz |
Built distributions (wheels)
Total release size: 22.8 MB
Release files / scribe_client-0.2.0.tar.gz
| Download URL | scribe_client-0.2.0.tar.gz |
|---|---|
| Size | 89.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f40488b96ec2fb00ee577b8eb80d512682723b458a190cfa6444942cdb321e19
|
|
BLAKE2b-256 checksum How to use checksums |
a2f26965d84f5fee79f0ae272b3e6acba7115b7eccf0f5a0c37feef767ac424f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logRelease files / scribe_client-0.2.0-cp39-abi3-win_amd64.whl
| Download URL | scribe_client-0.2.0-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 2.7 MB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2127eb6d8ad22f7bd54fd45e890db8b30128f8a0e774c16556288f94612a7770
|
|
BLAKE2b-256 checksum How to use checksums |
a92439572ec9f667556c7a3da0389e907138fdd85dbfe4d3a9b0abdd1697cc48
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logRelease files / scribe_client-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl
| Download URL | scribe_client-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 3.6 MB |
| Tags | CPython 3.9 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
fad7fea31e48ca2b46f0f40c3b08c9d58a786c04e3600aa5323ad9eeb9c5443a
|
|
BLAKE2b-256 checksum How to use checksums |
f2e8e8e2c4cc672a90c4e28c5087b90304722399e5f86af3d3cc0432618ff684
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logRelease files / scribe_client-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl
| Download URL | scribe_client-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 3.5 MB |
| Tags | CPython 3.9 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
8356fff61fa50f86e5e7aadc7a0c492f4244f037903bb518992c2ecf43bd3f04
|
|
BLAKE2b-256 checksum How to use checksums |
a9e5661fc4de91a5a6bbf12fd74a9d0866e3aaa0c8c0f482b1717ff4a26007da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logRelease files / scribe_client-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
| Download URL | scribe_client-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
|---|---|
| Size | 3.4 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
944caf747833b4cc3e690bd218153e4bd35008573a9d8d2f20d3beac7a7e81c9
|
|
BLAKE2b-256 checksum How to use checksums |
3e0dd2f128017958ef2299c300d48777cf9b8687a3407b53a6d14cccb1142fbc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logRelease files / scribe_client-0.2.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
| Download URL | scribe_client-0.2.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl |
|---|---|
| Size | 3.3 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
6fce475e8661d5b25e11848de69b0b78ac4ccd6f174932b153c395dd5cbdea70
|
|
BLAKE2b-256 checksum How to use checksums |
6e6075efdd3370abf1626e076d540b701b0c3fe20a9ea56deaeef6bac3f28bdd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logRelease files / scribe_client-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | scribe_client-0.2.0-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 3.0 MB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
a0970e3691a2de005b59adb7c4c11853888285bb75f741998b480c92f2ac7894
|
|
BLAKE2b-256 checksum How to use checksums |
177326a6239974455f473b8f99fc318eaf8cee487841a793dc94d08ed3b2a1d4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logRelease files / scribe_client-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | scribe_client-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 3.2 MB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
136861e1cbafb2dfb0ec9301e5415f4bd5e3b871ce4d7a395e6a36ee2828b3ad
|
|
BLAKE2b-256 checksum How to use checksums |
e238dd18e53fae9e9bc6163028db405dd7959c6d2fd47c07c58186bb475c6308
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logSecurity for scribe-client 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 8 files in this release have been uploaded with provenance metadata.
Attestation for scribe_client-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.