Skip to main content

craterview

Python client for the CraterView image enhancement and restoration API.

pip install craterview

Python 3.9 or newer. Three dependencies: requests for the API, and Pillow with pi-heif to read an image's size from its header — nothing is decoded — so a job's wait can be estimated for the picture you actually send, HEIC included.

Quickstart

from craterview import CraterView

cv = CraterView(api_key="cv_...")
result = cv.run("photo.jpg", scale=4)
result.save("photo-4x.jpg")

run() collapses the upload → submit → poll sequence into one call. That sequence is the API's shape rather than an accident, but it is not something every caller should have to reimplement.

Build against echo first. It costs no credits, needs no GPU and returns a real result — a plain upscale — so the request, the parameters and the response are the ones a paid model gives, and your integration needs no change when you switch. When you are ready, swap the model name for the one you want.

job = cv.run(
    # echo is free, for building against. Swap in a paid model when you are ready —
    # cv-enhance-v3 to enlarge, cv-restore-v1 to repair, cv-headshot-v1 for portraits.
    "image.jpg", model="echo",
    scale=2, wait=30,
)
job.save("image-2x.jpg")

A parameter one model publishes is not one another accepts — cv.models() says which — and a value a model does not publish is refused at submit rather than ignored.

An API key

Keys begin with cv_ and are issued from your dashboard. Pass one to the constructor; every call but the model catalogue needs one.

cv = CraterView(api_key=os.environ["CV_API_KEY"])

A key carries your whole allowance and does not expire, so keep it server-side. It is a credential for programs; it is not a login.

The one call

job = cv.run(
    image,                       # path, str, bytes, or a file object
    model="cv-enhance-v3",       # default
    wait=30.0,                   # seconds to hold the connection open
    timeout=600,                 # total seconds before giving up
    raise_on_failure=True,
    **params,                    # model parameters, e.g. scale=4
)

It waits server-side first, so a job that finishes quickly costs a single round trip, then falls back to polling for anything slower. With raise_on_failure=False a failed job is returned rather than raised, and job.error says why.

Which parameters a model accepts is published by the API rather than hard-coded here — see cv.models().

The explicit path

Useful when you want to hold the key, submit later, or fan out.

input_key = cv.upload("photo.jpg")                       # → "inputs/..."
job = cv.submit("cv-enhance-v3", input_key, scale=4)
job = cv.wait_for(job, timeout=600)
data = job.bytes()

Image bytes never pass through the API. upload() PUTs them straight to object storage on a presigned URL and bytes() GETs the result the same way; the API moves keys, not pixels.

Retries are yours to write

This client does not retry. That is deliberate: only the caller knows where one logical request ends and the next begins, and a retry the client invented would be a second job you are billed for.

What it gives you instead is a key generator. Make one key per logical request and reuse it for every attempt at that request:

from craterview import new_idempotency_key

key = new_idempotency_key()                # once, before the first attempt
for attempt in range(3):
    try:
        job = cv.submit("cv-enhance-v3", input_key, idempotency_key=key, scale=4)
        break
    except (ConnectionError, TimeoutError):
        continue                           # same key, so at most one job is ever created

The two it catches are ConnectionFailed and TimedOut, raised when a request got no answer; each is also the builtin the loop names. An answer from the API, even a refusal, is not a reason to send the request again.

Generating a fresh key per attempt defeats the point entirely — the server has nothing to match against and every attempt starts its own job. Reusing a key with a different body is rejected with 409 rather than quietly handing back the earlier result. Claims are kept for as long as the job's record is, which is not deleted; the same key always returns the same job.

Errors

Everything raised by this client descends from CraterViewError, so one except catches the lot. .status carries the HTTP status where there was one, and .trace_id the API's X-Trace-Id for a request it answered and refused — the id to quote when you ask us about it.

Exception Meaning
RateLimited 429 — one of five limits, and the message says which: the key's request rate, the account's upload URLs a minute, the account's new keys an hour, the account's jobs in flight, or a full queue. .retry_after is seconds to wait: until the window rolls over for the two per-minute limits, until a new key can be made for the hourly one, or an interval to poll on for the other two.
JobFailed The job ran and did not succeed. .args[0] says what you can do about it; .error_code is the half to branch on.
ConnectionFailed The request never reached the API or storage: refused, reset, or a name that would not resolve. Also a builtin ConnectionError. Safe to repeat with the same idempotency key.
TimedOut No answer within the client's timeout. Also a builtin TimeoutError. The request may have arrived, so repeat a submission with the same idempotency key, never a new one.
CraterViewError Everything else, including 4xx and 5xx from the API, and storage refusing an upload or a download.

Job

Every field the API publishes on a job is exposed here.

id, model, status status is one of queued, running, succeeded, failed
created_at Timezone-aware datetime
succeeded, done done covers both terminal states
error Set when the job failed. Safe to show a user
error_code The same fact, as a stable identifier. Branch on this, show the other
credits What you were billed
eta_seconds Seconds until the job is expected to finish, recomputed on every read — it counts down while the job runs. Absent once the job has settled. Estimated for your image's size when upload() could read it — its header is read with Pillow, nothing is decoded — or when you pass input_megapixels to submit(); for a typical image otherwise
community True when the job is on the community queue, which runs on shared, free capacity and can wait longer at busy times
output_url, download_url The result, presigned. One to display, one to save
thumbnail_url A small JPEG of the job's picture, for listings — the result, or what the model worked from when it produced no file. Null when none was drawn
input_url The picture the model worked from — the region, where you named one
alpha_url Only for a JPEG result of a picture with transparency, which JPEG cannot hold: the transparency as a grayscale JPEG, white where opaque. The result is then the colour alone
output_content_type The result's media type
output_bytes The result's size in bytes
bytes(), save(path) Download the result

result is the whole of what the job produced, and the rows above it that describe the result's file are accessors onto result["output"] rather than separate fields — the API states those links once. A model with no file to hand back returns its answer in result and leaves every one of them null; the fields it answers with are its result_schema in cv.models().

credits is the only figure about cost the API states, and the price is fixed and published per model, so an invoice reconciles against credits alone. For how long a job took, subtract created_at from finished_at — that is the wait you had, queue included.

Uploads declare their size. upload() and run() measure the bytes they are about to send and declare that figure; the API signs it into the presigned URL, so storage accepts exactly that length and nothing else. You do not pass it — it is read off the data in hand, which is what makes it impossible to get wrong.

Running out of credit does not stop you. A job submitted against a balance of zero is accepted and run, and charged when it succeeds — it goes to the community queue, which runs on shared, free capacity and can wait longer at busy times. It comes back with community set. There is no payment error to handle: paying — with credit, or with a subscription — moves work onto paid compute that scales with demand, rather than buying the right to submit. eta_seconds covers the whole wait, queue time included, so a community job simply reports a longer one.

output_url and download_url are the same object signed two ways; the content disposition is signed in, so the second cannot be derived from the first. Both expire, so fetch the result rather than storing the link.

thumbnail_url and input_url are for building a job listing: a few-hundred-pixel preview so a page of jobs costs kilobytes, and the picture the model worked from so a result can be shown against it. Where you named a region, input_url is that region — so a before-and-after is a true pair, and what was used is something you can look at rather than something to take on trust. It is not the file you uploaded: yours stays yours and is removed on its own schedule. Both expire with the job's other pictures. A model that produces no file has no result to preview, so thumbnail_url is a preview of the picture it answered about.

Webhooks

Pass webhook_url on submit — an absolute http or https URL, refused at submit if it is not one — and we POST to it once the job settles. Verify every delivery before you act on it: anyone who learns your URL can send you a plausible-looking body.

from craterview import CraterView, verify_webhook, InvalidSignature

secret = CraterView(api_key="cv_...").webhook_secret()   # created the first time you ask

@app.post("/hooks/craterview")
def hook(request):
    try:
        event = verify_webhook(request.body, request.headers, secret)
    except InvalidSignature:
        return 400                       # not from us; do nothing at all

    # Answer now and work afterwards. A slow endpoint is retried while it is still
    # working, so treat deliveries as at-least-once and make this idempotent on `id`.
    enqueue(event["id"])
    return 200

Every copy of a delivery states the same thing, so the first one you accept is the whole answer — later copies of an id you have already handled can be dropped rather than reconciled.

Pass the raw bytes. Most frameworks parse JSON for you, and re-serializing it changes the bytes the signature was computed over. In Flask that is request.get_data(), in Django request.body, in FastAPI await request.body().

The body carries the same fields a job read does, by the same names — including result and the links inside it. The one difference is what happens to a field that does not apply: a callback states every key and sets it null, where a job read leaves it out. So a subscriber parses one shape and never has to ask which keys arrived.

The links expire like any others. A delivery normally arrives within seconds of the job settling and they are good for an hour, but a subscriber that queues work for later should read the job again rather than storing them.

Rotating the secret

new = cv.rotate_webhook_secret()

The old secret keeps being sent alongside the new one for a day, and verify_webhook accepts either — so deploy the new one whenever suits rather than the moment you asked for it.

Managing keys

cv.keys()                    # every key on the account, by prefix; revoked ones stay listed
cv.create_key("staging")     # the value is in this response and nowhere else
cv.revoke_key("key_...")     # immediate, and not reversible

Several keys on one account is the ordinary arrangement — one per service or environment, so revoking one leaves the others working. They share the account's credits and history.

To rotate without downtime: create the new key, move your clients onto it, then revoke the old one. You cannot revoke the key you are calling with.

Everything else

cv.models()                       # models, what each takes and answers with, and which queue you are on
cv.jobs(limit=50, status="succeeded")   # your history, newest first, paging transparently
cv.job("job_...")                 # one job by id
cv.usage()                        # credit balance, spend and job counts

jobs() is a generator and pages for you. limit is how many jobs one request fetches, not how many you get: it keeps going until your history runs out. 200 is the largest page the API serves, and a bigger number is fetched as 200.

Configuration

CraterView(
    api_key=None,
    base_url="https://api.craterview.ai",
    timeout=60.0,                 # per-request, seconds
)

base_url is what you change to point at a local server.

From Claude Code

This repository is also a Claude Code marketplace. Two plugins: craterview connects CraterView's hosted tools so the assistant enhances images in the conversation, and craterview-api teaches an agent to call the API from code with this client. See plugins/ for what each does and how it finds a key.

/plugin marketplace add craterviewai/craterview-python
/plugin install craterview@craterviewai
/plugin install craterview-api@craterviewai

Versioning

Semver, from 0.1.0 on. A release that removes a public name, renames an attribute on Job or changes the type of one, gets a minor bump while this is 0.x and a major bump after 1.0.0; anything purely additive gets a patch. Pin what you depend on.

The API this wraps adds fields to its responses without warning, so treat an unfamiliar key in result or a status you do not recognize as something to ignore rather than to fail on.

License

Apache-2.0. See LICENSE.

Metadata

Release files for craterview 0.4.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for craterview 0.4.6
File Size Uploaded
craterview-0.4.6.tar.gz 29.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for craterview 0.4.6
File Interpreter ABI Platform
craterview-0.4.6-py3-none-any.whl Python 3 none any Details

Total release size: 54.2 kB

Release files / craterview-0.4.6.tar.gz

Download URL craterview-0.4.6.tar.gz
Size 29.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e354c5724eabb48727c840dafe099636091c9609339cc07f3c6cb7a2cf80a777
BLAKE2b-256 checksum
How to use checksums
48f682478aec79ac44aeaf3b3d7e757cb3da9586f1bf98f1419754f629aebe93
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 3, 2026.

Transparency log

Release files / craterview-0.4.6-py3-none-any.whl

Download URL craterview-0.4.6-py3-none-any.whl
Size 24.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d20eee7128c2b7dfb6570f6c3364f85538ddba77949c751000b19c09c0197b9f
BLAKE2b-256 checksum
How to use checksums
280256d883aba1cf514eb0568d508ba6fb9646866e7a7f138d13359c6dd54496
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 3, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.20

2 release files

0.4.19

2 release files

0.4.18

2 release files

0.4.9

2 release files

0.4.8

2 release files

0.4.7

2 release files

This release

0.4.6 This release

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.19

2 release files

0.3.18

2 release files

0.3.16

2 release files

0.3.13

2 release files

0.3.12

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.1.5

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page