Machinera Python SDK
Python client for the Machinera speech-to-text API, with a blocking client
(Machinera) and a native asyncio client (AsyncMachinera). Requires Python 3.10
or newer.
Quick start
pip install machinera
export MACHINERA_API_KEY="your-api-key"
from machinera import Machinera
with Machinera() as client:
result = client.transcribe_file("recording.wav", model="transcribe-v1")
print(result.text)
transcribe-v1 is the model ID. Each call waits for the finished transcript and
returns a TranscriptionResult. To transcribe audio that is already online, pass
a direct audio URL instead:
with Machinera() as client:
result = client.transcribe_url("https://audio.example/recording.wav", model="transcribe-v1")
Supported formats are the file suffixes in machinera.SUPPORTED_MEDIA_SUFFIXES;
content without a supported name is identified from its MIME type or container
signature (see File inputs). Large files are uploaded automatically (see
Large files). Live calls submit audio for transcription and may
incur usage charges.
Asyncio
AsyncMachinera takes the same configuration and has the same methods; await them:
import asyncio
from machinera import AsyncMachinera
async def main() -> str:
async with AsyncMachinera() as client:
result = await client.transcribe_file("recording.wav", model="transcribe-v1")
return result.text
text = asyncio.run(main())
Recover an interrupted transcription
A call can end before it returns a transcript (a deadline, Ctrl+C, a network
failure, or task cancellation) while the service keeps working on the job. The SDK
never cancels a server job or deletes its input. To pick the work up again, save
an operation key before calling and pass it as idempotency_key:
import uuid
from machinera import APIError, Machinera
key = uuid.uuid4().hex
save_recovery_state(key=key, job_id=None) # your own durable storage
with Machinera() as client:
try:
result = client.transcribe_file("recording.wav", model="transcribe-v1", idempotency_key=key)
except APIError as error: # includes DeadlineExceededError and TranscriptionInterrupted
save_recovery_state(key=key, job_id=error.job_id)
raise
Later, even from a new process, continue from the saved state:
with Machinera() as client:
if saved_job_id is not None:
result = client.resume(saved_job_id) # polls the accepted job; never submits
else:
result = client.transcribe_file( # replays the same submission
"recording.wav", model="transcribe-v1", idempotency_key=saved_key
)
The rules behind this recipe:
- Keyed calls are durable jobs. An
idempotency_key(ortransport="job") submits a server job that can be resumed. Without one, a file whose encoded request fitsLimits.sync_inline_body_bytesis sent as a single synchronous request. If that request's response is lost, the SDK raisesAmbiguousSubmissionError: the transcription may already have run, so reconcile (check what you already received or were billed for) instead of resubmitting. - Reuse a key only for the identical request: same file bytes, metadata,
options, credentials, and endpoint. Use a fresh key for every independent
transcription. Every
APIErrorcarries the call's key inoperation_key, including the random key generated when you passed none. - Once a job ID is known, use
resume(job_id). It only polls and never creates a replacement job.get_job(job_id)reads a single status snapshot (JobSnapshot). - A failed job raises
TerminalJobErrorand is never resubmitted automatically. - Save results promptly. The service retains results for a limited time, and recovery does not extend it. An expired result or replay requires reconciliation, not a new submission disguised as a retry. The SDK keeps no journal on disk.
What to do after a failure; use the first row that matches:
| Exception | Meaning | Next step |
|---|---|---|
TerminalJobError |
The job failed. | Inspect code before deciding on a new job. |
AmbiguousSubmissionError |
An unkeyed synchronous request may have run. | Reconcile; do not resubmit. |
AuthenticationError, PermissionDeniedError |
The credential was refused. | Check the API key and its access. |
BadRequestError, UnprocessableEntityError, PayloadTooLargeError |
The request is invalid. | Correct the input. |
IntegrityError, other UploadError |
The file changed, or storage or the service refused the upload. | Keep the file unchanged and see Large files. |
TranscriptionInterrupted with ambiguous true |
An unkeyed synchronous request was interrupted and may have run. | Reconcile; do not resubmit. |
Any other APIError except RateLimitError with phase == "sync_submit" |
An unkeyed synchronous request failed after it may have run. | Reconcile; do not resubmit. |
NotFoundError, ConflictError |
The job is unknown to this credential and endpoint, or the service cannot replay the key. | Reconcile; do not resubmit under a new key. |
Any other APIError with job_id set |
The job was accepted and may still be running. | resume(job_id) |
DeadlineExceededError, TranscriptionInterrupted, APIConnectionError, APIResponseValidationError, RateLimitError, InternalServerError |
Admission was not confirmed. | Repeat the identical call with idempotency_key=error.operation_key, after a pause for rate limits and server errors. |
Any other APIError |
The service refused the operation. | Inspect code and reconcile before any new submission. |
With AsyncMachinera, task cancellation propagates the original
asyncio.CancelledError with operation_key, upload_id, phase, job_id, and
last_status attached once the call has started. Read them with
getattr(error, "job_id", None) inside the coroutine that directly awaits the SDK
method, save them, and re-raise. On Python 3.10, a task boundary can replace the
cancellation exception, so another task may not see these attributes.
For files above the inline limit, see Large files. Complete recovery programs: blocking, asyncio, and large file.
Configuration
All constructor arguments are keyword-only. Explicit non-None values take
precedence over the environment, and invalid values raise ValueError without
falling back.
api_keydefaults toMACHINERA_API_KEY. A missing or empty credential raisesValueErrorbefore any request.base_urldefaults toMACHINERA_BASE_URL, thenhttps://api.machinera.com/v1. An origin without/v1is accepted and normalized.timeout,max_retries, andretry_policyare described under Timeouts and deadlines and Retries.default_headersis copied and merged into API requests. It can override non-reserved defaults such asUser-Agent; invalid headers and attempts to set a header the SDK owns (_RESERVEDin_files.py) raiseValueErrorbefore any request.transport="job"sends every call as a durable job. The default,"auto", picks synchronous, inline durable-job, or staged submission by encoded size.limits=Limits(...)changes the encoded request sizes behind that choice; it does not change service limits. Durable jobs aboveLimits.job_inline_body_bytesuse staged uploads (see Large files).max_concurrencyoptionally bounds the number of active calls; waiting for a slot counts toward the call's deadline.
No dotenv files are loaded and no endpoints are probed. Client configuration is
immutable. One Machinera client can be shared by threads, and one
AsyncMachinera client by tasks on one event loop. close() / aclose() (or
leaving the with block) waits for active calls and closes only an HTTP client the
SDK created.
File inputs
transcribe_file accepts a path (str or PathLike; bare strings always mean
paths), bytes, a seekable binary handle, or a tuple
(filename, content[, content_type[, headers]]) whose content is any of those.
Content is read from the handle's current offset to its end and streamed without
decoding or conversion.
with Machinera() as client:
result = client.transcribe_file(audio_bytes, model="transcribe-v1", filename="clip.wav")
result = client.transcribe_file(
("clip.flac", audio_bytes, "audio/flac", {"X-Part-Label": "recording"}),
model="transcribe-v1",
)
The upload name is resolved in this order:
- An explicit
filename=or tuple filename; an unsupported suffix raisesValueError. Conflicting non-Nonetuple and keyword values also raise. - The basename of a path or handle name, when its suffix is supported. Numeric handle names and unsupported suffixes are ignored.
- For unnamed content, a supplied MIME type, mapped by
_MIME_SUFFIXESin_files.py. - Otherwise, a container signature in the leading bytes, read by
Multipart.preparein_multipart.pyand matched bysniffin_files.py.
Content that cannot be identified raises ValueError before any request. Names,
content types, and part headers are validated against header injection, and part
headers cannot carry credentials or cookies or replace multipart framing.
Binary handles stay open, and their offset is restored after an ordinary completion. Do not modify a file during a call or share one handle between simultaneous calls.
With Machinera, a deadline or interruption can end the call while a read or seek on
your handle is still blocked. Call error.wait_for_file_release(timeout) on the
raised APIError and require True before reusing, seeking, or closing the handle
(timeout=0 only checks). The SDK does not restore the offset in that case.
With AsyncMachinera, task cancellation raises asyncio.CancelledError, which has
no wait_for_file_release. None is needed: before the cancellation propagates, the
SDK awaits any in-flight file operation and restores a caller-owned handle's
original offset (or closes a file it opened). See the
asyncio cleanup contract.
Timeouts and deadlines
Every call is bounded by a total deadline; the defaults are declared by the
TimeoutPolicy fields.
The deadline is monotonic and covers preparation, waiting for a concurrency slot,
requests, retry sleeps, and polling. Each poll request also has its own limit.
timeout= |
Effect |
|---|---|
| omitted | Inherit the client's policy (the method) or TimeoutPolicy() (the constructor). |
| seconds | Set the connect, write, read, and pool phases to that value. |
httpx.Timeout(...) |
Copy its four phases, including disabled (None) ones. |
None |
Disable HTTP phase limits only. |
TimeoutPolicy(...) |
Replace all six values. |
The scalar, httpx.Timeout, and None forms keep the inherited poll-request and
total-deadline bounds, so calls stay bounded. transcribe_file, transcribe_url,
and resume also accept deadline=seconds to override the total budget for one
call. HTTP phase values follow
httpx timeout semantics.
A deadline raises DeadlineExceededError (or AmbiguousSubmissionError for an
unkeyed synchronous request that may have started) and keeps the operation key and
any accepted job ID for recovery.
Retries
Retry defaults are declared by the
RetryPolicy fields.
max_retries=n allows n + 1 attempts per replay-safe step, and 0 disables
retries; pass retry_policy=RetryPolicy(...) instead for full control (supplying
both raises ValueError). Polling has its own interval and count budget.
Network failures, HTTP 429/502/503/504, and service refusals marked retryable are
retried only when replaying the request is safe. Backoff is
min(initial_delay * 2**retry_index, max_delay) times a random factor in
[0.75, 1]; a Retry-After header sets a minimum wait, and a wait that cannot
finish before the deadline raises DeadlineExceededError. Retries resend the same
encoded body and operation key. Authentication and permission failures and failed
jobs are never retried. The service's retryable flag takes precedence over the
SDK's error-code table, which in turn takes precedence over the status-code rule.
Results
transcribe_file, transcribe_url, and resume return TranscriptionResult;
get_job returns JobSnapshot. Both are frozen Pydantic models with attribute
access. Unknown response fields are kept in raw, and malformed responses raise
APIResponseValidationError rather than a Pydantic ValidationError.
JobSnapshot.status is a plain string, so a status added by the service later still parses; while
polling, any status other than queued, processing, or completed raises
TerminalJobError.
with Machinera() as client:
snapshot = client.get_job(saved_job_id)
print(snapshot.status)
result.text preserves whitespace and empty strings exactly. output follows the
requested response_format: to_json() returns the text and any usage,
to_text() the exact text, and to_verbose_json() every returned field. Durable
jobs return verbose fields whatever format was requested; a synchronous response
contains only what the service sent. elapsed_seconds is the whole call as seen by
the client, including preparation, retries, and polling.
Errors
Every SDK failure derives from MachineraError, and every API or transport failure
from APIError. Local argument errors raise ValueError or TypeError before any
request. APIError carries status_code (alias status), code, retryable,
request_id, a sanitized body, and the recovery context operation_key,
job_id, upload_id, phase, and last_status.
HTTP failures raise APIStatusError subclasses (BadRequestError,
AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError,
PayloadTooLargeError, UnprocessableEntityError, RateLimitError,
InternalServerError). APIConnectionError and its subclass APITimeoutError
cover transport failures. APIResponseValidationError means the service returned a
body the SDK cannot use and is never retried automatically. A failed job whose
uploaded file did not match raises TerminalIntegrityError, which is both an
IntegrityError and a TerminalJobError. TranscriptionInterrupted is both an
APIError and a KeyboardInterrupt. The
recovery table gives the next step for each
class, the full hierarchy is in the
API reference,
and handle_errors.py
prints guidance for a failed call.
Exceptions and their chains never contain service free text, raw HTTP objects, URLs, HTML, audio, or transcripts. Protect credentials, source URLs, operation keys, audio, and transcripts in your own logging; see Logging for the SDK's own records.
Logging
The SDK logs to the standard logging logger named machinera: retry decisions at
DEBUG (method, path template, HTTP status, error code, attempt, delay, and request
ID) and job status changes while polling at INFO (job ID and status). Records
never contain URLs with query strings, credentials, file paths, or transcript text.
Set MACHINERA_LOG=debug or MACHINERA_LOG=info to set that level when a client is
created; a stderr handler is attached only if the logger has none. Configure the
logger directly for anything else.
Large files
When the encoded request exceeds the inline limit for durable jobs
(Limits.job_inline_body_bytes, which defaults to the SDK's staged-upload threshold
STAGED_UPLOAD_THRESHOLD_BYTES in
_types.py),
transcribe_file uploads the file to storage and then submits a job that references
it. This happens with either transport setting; a body exactly at the limit is
still sent inline. The service supplies the upload size limit and expiry window. If
the service refuses the upload with staged_uploads_unavailable before granting it,
the SDK submits the same body once as an inline durable job under the same operation
key, provided it fits the service's inline limit; a larger file fails with that
error. Byte limits are separate from audio duration and account limits.
File preparation and streaming are implemented in
_multipart.py,
and the staged flow and its inline fallback in
_core.py.
Changes to the file's size or content raise IntegrityError. Keep the input
unchanged through retries and recovery.
To recover a staged file whose job ID is not yet known, pass the same file, options,
and key to resume. It replays initialization, upload, and submission
idempotently and never generates a new key:
with Machinera() as client:
result = client.resume(
file="recording.flac",
model="transcribe-v1",
operation_key=saved_key,
upload_id=saved_upload_id, # optional, from error.upload_id
)
Pass the original response_format and language as well: when omitted, recovery
before admission uses "json" and no language hint. Options that differ from the
original call raise an error with code idempotency_payload_mismatch instead of
returning the earlier result. Once a job ID is known, resume(saved_job_id) only
polls.
Storage failures raise UploadError, whose storage_code holds only a sanitized
storage error code. Service errors whose code starts with upload_ also raise
UploadError, with status_code set, except rate limits, which raise
RateLimitError. An expired upload grant is refreshed automatically within the fixed
upload window, and an incomplete upload is retried once. Integrity mismatches,
expired or already-bound uploads, and key mismatches need reconciliation; an expired
upload cannot be reopened under the same key.
HTTP client
The SDK chooses its HTTP clients in Core.__init__ in
_core.py.
An injected http_client or custom transport carries every request, and its own
pool behavior applies. Otherwise uploads and submissions use a client whose pool
(EXCHANGE_POOL) keeps no idle connections, so a connection closed at a deadline is
never reused, and job status reads use a separate client with POLL_POOL. The
blocking client builds that status-read transport with keepalive_transport in
_io.py,
which falls back to EXCHANGE_POOL when it cannot install its connection hook.
Clients the SDK creates ignore environment proxies, do not follow redirects, and use
httpx's default of zero transport retries.
close() and aclose() close only the clients the SDK created.
An injected http_client=httpx.Client(...) (or httpx.AsyncClient for
AsyncMachinera) keeps its own pool, proxies, URL mounts, and lifetime, and is used
for both API calls and storage uploads. Upload requests are sent with auth=None
and without redirects, so the client's default headers, authentication, and cookies
are not added to them. Request and response hooks still run for uploads: they must
keep the upload headers, add no credentials or cookies, and must not log the signed
upload URL. The SDK suppresses httpx's own request log for uploads.
For offline tests, pass an httpx.BaseTransport (or httpx.AsyncBaseTransport) as
transport; clock, sleeper, wall_clock, and random_source are injectable
too.
When a call is cancelled, the SDK closes the in-flight connection or response stream
and makes no further requests. A custom transport that cannot be interrupted may
finish in a background daemon thread, and its late result is discarded. In
AsyncMachinera, file hashing, inspection, and reads run in worker threads, so slow
local I/O can delay cancellation without blocking the event loop.
Versioning and requirements
Releases use semantic versions. During 0.x, minor releases may change the public
API, and patch releases contain compatible fixes; from 1.0, incompatible changes
require a major release. Read the
changelog
when upgrading, and pin an exact version where installs must be reproducible.
The SDK is tested on Python 3.10 through 3.14. Its runtime dependencies are declared
in pyproject.toml;
it needs no audio decoder or conversion tools.
See the API reference, the runnable examples, CONTRIBUTING.md for development, and SECURITY.md for reporting vulnerabilities.
Metadata
Release files for machinera 0.1.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 | |
|---|---|---|---|
| machinera-0.1.0.tar.gz | 184.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| machinera-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 232.2 kB
Release files / machinera-0.1.0.tar.gz
| Download URL | machinera-0.1.0.tar.gz |
|---|---|
| Size | 184.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a40eeaa33d03c6df76f0443ccdf1973d33246ddeaff4d0602f313119df2d4cfd
|
|
BLAKE2b-256 checksum How to use checksums |
0429bbc3a9b051dd9b113b8b4d3feebaa76328ab539a2fc6950158cc4ca6d19c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 2, 2026.
Transparency logRelease files / machinera-0.1.0-py3-none-any.whl
| Download URL | machinera-0.1.0-py3-none-any.whl |
|---|---|
| Size | 48.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d4d36c0280a870726902bc57c551e5e3e9376c9e4351c3252058a6878c79f74c
|
|
BLAKE2b-256 checksum How to use checksums |
4335f8e598ae20f1901ea61a552558ecaf3fd6d328ff1ef5ab155dc206868c87
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 2, 2026.
Transparency log