Parse a Sume video poll response with a dataclass, no network

A stdlib Python dataclass for the /v1/videos poll response (status, unsigned_urls, usage.cost, error), tested on sample JSON so a ported client fails early.

5 min readSume
All posts

A ported video client breaks quietly when it reads a field the new API names differently. Parse the Sume poll response once, into a small dataclass, and the rest of your code touches typed attributes: id, status, model, unsigned_urls and usage_cost. The docs show a completed response with exactly those keys plus generation_id and polling_url (Sume video docs, read 2026-10-06).

The OpenAI Sora API ended on 2026-09-24 (Magic Hour tracker, read 2026-10-06), which leaves many wrappers whose response handling was written for a different shape. A parser plus a sample file is the cheapest test you can add during the move.

The fields that matter

Poll response fields, Sume docs, read 2026-10-06
FieldTypeWhen presentUse it for
idstringAlwaysYour job key in your database
statusstringAlwayspending, in_progress, completed, failed or cancelled
polling_urlstringAlwaysWhere to poll next
unsigned_urlsarray of stringsOn completedDownload the video; index 0 is the first output
usage.costnumberOn completedThe Sume billable amount in USD
errorstring or objectOn failedDecide retry or give up

The parser

It does not call the network. Feed it the JSON from the docs, from a stored response, or from a live poll.

import json
from dataclasses import dataclass, field

@dataclass(frozen=True)
class VideoJob:
    id: str
    status: str
    model: str
    urls: list[str] = field(default_factory=list)
    cost: float | None = None
    error: object = None

    done = property(lambda s: s.status in {"completed", "failed", "cancelled"})

def parse(raw: str) -> VideoJob:
    d = json.loads(raw)
    for key in ("id", "status"):
        if key not in d:
            raise ValueError(f"poll response lacks {key}")
    cost = (d.get("usage") or {}).get("cost")
    return VideoJob(d["id"], d["status"], d.get("model", ""),
                    d.get("unsigned_urls") or [], cost, d.get("error"))


sample = '{"id":"job_1","status":"completed","model":"seedance-2","unsigned_urls":["https://api.sume.com/v1/videos/job_1/content?index=0"],"usage":{"cost":0.25}}'
j = parse(sample)
assert j.done and j.cost == 0.25 and len(j.urls) == 1
print(j)

What it catches

  • A missing id or status raises at the edge, not three functions later.
  • A completed job with no unsigned_urls shows as an empty list, which your caller can treat as an error instead of downloading nothing.
  • cost stays None until completion, so a budget total never adds a half-known number.
  • A new status word needs one edit in done; pair it with the normalizer from the statuses post if you also poll the jobs route.

Downloading

Take urls[0] and fetch it. The docs show the content endpoint also accepts the same bearer key, with an index query parameter that defaults to 0 for models that return more than one output. Save the bytes to your own storage straight away and store your own path next to the job id; do not rely on a URL staying valid for your customers.

Add three more sample files to your test folder: a pending response, a failed one with an error, and a cancelled one. Four small JSON files make a contract test that runs in milliseconds. The earlier posts on a pytest contract test and on unsigned URLs go further on mocking and download behavior.

Fixtures for the contract test

The parser is only as good as the responses you test it with. Keep one fixture file per terminal state in a fixtures/ folder: pending, in progress, completed, failed with an error, and cancelled. Take the completed one from the docs example, and take the others from a real job of your own, with the id and URLs replaced by placeholders.

A test then loads each file, parses it, and checks a few facts: the normalized state, whether unsigned_urls is empty, and whether cost is set. Run it in your normal test suite. It takes milliseconds, needs no key, and fails the day someone edits the parser or the docs add a field you should handle.

If you prefer a validation library to a dataclass, the idea is the same: parse at the edge, fail loudly on a shape you do not know, and give the rest of the program a typed object. The standard library is enough here and adds no dependency to a small worker, which is why the example uses it.

  • Add a fixture for an unknown status word and assert that the parser raises.
  • Add a fixture where usage is missing on a completed job, to decide what your code does with a gap.
  • Keep the real poll responses you log in production for a few days; they are the best source of new fixtures when something surprising shows up.
  • Do not put a real signed or authenticated URL into a fixture that lives in a repository.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume