Full code-level plan: docs/superpowers/plans/2026-07-07-v2-parse-extract-ade-python.md (13 TDD tasks with complete code + test cycles). This issue is the summary — GitHub's 64 KB body limit won't hold the full plan.
Scope: ade-python only (TS is a separate port). Additive client.v2 sub-client for V2 parse, V2 extract, their async job routes, and file staging — routed to the ADE host — with hand-written ergonomics (unified Job, wait() polling, pydantic-schema acceptance, save_to parity).
Contract verification (against the real openapi.json)
Verified all 13 routes. Several items in the design doc's table were stale/wrong; the plan encodes the corrected reality:
idempotency_key is extract-only — absent from every parse route in the spec.
- 206 partial-success is on parse, not extract.
POST /v2/parse returns 206 with metadata.failed_pages; POST /v2/extract returns only 200/422 — strict-mode failure is 422.
- Encrypted PDFs are supported — both parse routes accept
password. (Doc item "password → 422" is obsolete.)
- Parse result is untyped in the spec (prose only); extract is typed (
V2ExtractResult/V2ExtractMetadata). V2 parse metadata differs from V1's.
- Envelope divergence is worse than flagged:
|
parse job |
extract job |
| timestamps |
received_at, created_at — int epoch secs |
created_at, completed_at — ISO strings |
| terminal payload |
data (inline) or output_url |
result |
| failure |
failure_reason (string) |
error {code, message} |
| status enum |
+cancelled |
no cancelled |
| list envelope |
{jobs, org_id, has_more} |
{jobs, page, page_size, has_more} |
Even within extract, list uses failure_reason but get uses error{code,message}.
/v1/files: single file part in, open {string:string} map out (file_ref is a key). Lives on the ADE host.
/v2/workflow: full third parallel surface (sync + jobs + get + list). Out of scope here, but the resource/waiter machinery accepts it later with no rework.
- No cancel endpoint despite parse's
cancelled status. Sync routes can 504 on timeout (extract 504 ⇒ workflow cancelled; retry w/ same idempotency_key restarts).
Design decisions
- Unified
Job shape (chosen): one normalized model for parse + extract — status (common enum), created_at/completed_at (datetime, both int-epoch and ISO normalized via the existing parse_datetime), progress (float), result (typed: V2ParseResponse | V2ExtractResult), error (JobError), plus a .raw escape hatch for envelope-specific fields (org_id, output_url, version). Stable if aide unifies the API later.
- Routing needs no new HTTP client. The transport's
_prepare_url passes absolute URLs through untouched, so v2 resources build absolute URLs against a resolved _v2_base_url and inherit auth/retries/http client. /v1/files routes to the ADE host despite its /v1 path.
- Environment = the whole user-facing story. 4-entry matrix of (V1, V2) host pairs;
LANDINGAI_ADE_ENVIRONMENT env var; explicit base_url/v2_base_url overrides; if only base_url is set, V2 follows it (one mock captures everything).
- Generated-file edits are minimal & additive —
_client.py (env map, v2_base_url resolution, one v2 cached property per client, widened environment Literal) + two re-exports. All other V2 code is new files under types/v2/, resources/v2/, lib/. Holds whether or not the Stainless exit ("Problem 1") has landed.
- Schema acceptance:
v2.extract takes a pydantic model class, a dict, or a JSON string (extends lib/schema_utils.py), sent as a JSON object in the request body.
Environment matrix
| environment |
V1 base URL |
V2 base URL |
| production (default) |
https://api.va.landing.ai |
https://api.ade.landing.ai |
| eu |
https://api.va.eu-west-1.landing.ai |
https://api.ade.eu-west-1.landing.ai |
| staging |
https://api.va.staging.landing.ai |
https://api.ade.staging.landing.ai |
| dev |
https://api.va.dev.landing.ai |
https://api.ade.dev.landing.ai |
Task breakdown (13 TDD tasks)
Each task ends with an independently testable deliverable and a commit. Full code in the plan doc.
- Environment map + dual base-URL resolution —
_client.py (sync+async): 4-pair map, v2_base_url param, LANDINGAI_ADE_ENVIRONMENT/LANDINGAI_ADE_V2_BASE_URL, _v2_base_url attr, widened environment Literal, copy(). Tests: tests/test_v2_environment.py.
- V2 types —
types/v2/{job,parse_response,extract_response,file_upload_response}.py: JobStatus/JobError/Job, V2ParseResponse+metadata+billing, V2ExtractResult+V2ExtractMetadata, V2FileUploadResponse. Tests: tests/test_v2_types.py.
- Job normalizers —
resources/v2/_normalize.py: normalize_parse_job, normalize_extract_job (epoch vs ISO, data/result, failure_reason/error). Tests: tests/test_v2_normalize.py.
- Schema coercion —
lib/schema_utils.py: coerce_schema_to_dict(pydantic|dict|str) -> dict. Tests: tests/test_v2_schema.py.
- V2 errors + 504 wrapper —
lib/v2_errors.py: V2SyncTimeoutError, JobWaitTimeoutError, JobFailedError, raise_if_sync_timeout. Tests: tests/test_v2_errors.py.
v2 sub-client + routing + waiter — resources/v2/{_base,v2}.py: V2ResourceMixin._v2_url, poll_until_terminal/apoll_until_terminal (injected clock/sleep), V2Resource/AsyncV2Resource, client.v2 cached properties. Tests: routing via respx.
client.v2.files.upload — resources/v2/files.py → returns file_ref, hits ADE host. Tests: tests/api_resources/v2/test_files.py.
client.v2.parse (sync) — resources/v2/parse.py: options JSON-encoded, 206 returns response with failed_pages, 504 → V2SyncTimeoutError, save_to. Tests: tests/api_resources/v2/test_parse.py.
client.v2.parse_jobs — create/list/get/wait returning normalized Job; JobList carries has_more/org_id; priority, output_save_url. Tests appended.
client.v2.extract (sync) — resources/v2/extract.py: JSON body, schema coercion, idempotency_key, options.strict, 504 handling, save_to. Tests: tests/api_resources/v2/test_extract.py.
client.v2.extract_jobs — create/list/get/wait; maps error{code,message}, page/page_size envelope. Tests appended.
- Async parity + full suite/lint/typecheck — async smoke tests;
rye run pytest -q; rye run lint. Tests: tests/api_resources/v2/test_async_smoke.py.
- Docs & examples —
api.md V2 section, README V2 subsection, examples/v2_parse.py, examples/v2_extract.py.
Open items for the aide team (non-blocking)
- Parse
idempotency_key: absent from parse routes though present on extract — confirm intent.
- Envelope unification before GA (normalizers collapse toward identity; public
Job shape unaffected).
/v2/workflow scope confirmation.
- Typed parse response:
V2ParseResponse is permissive until the gateway publishes a typed schema.
- 504 retries: transport retries 5xx (incl. 504) before conversion — consider suppressing retries for sync 504s in a follow-up.
🤖 Generated with Claude Code
Full code-level plan:
docs/superpowers/plans/2026-07-07-v2-parse-extract-ade-python.md(13 TDD tasks with complete code + test cycles). This issue is the summary — GitHub's 64 KB body limit won't hold the full plan.Scope: ade-python only (TS is a separate port). Additive
client.v2sub-client for V2 parse, V2 extract, their async job routes, and file staging — routed to the ADE host — with hand-written ergonomics (unifiedJob,wait()polling, pydantic-schema acceptance,save_toparity).Contract verification (against the real
openapi.json)Verified all 13 routes. Several items in the design doc's table were stale/wrong; the plan encodes the corrected reality:
idempotency_keyis extract-only — absent from every parse route in the spec.POST /v2/parsereturns 206 withmetadata.failed_pages;POST /v2/extractreturns only 200/422 — strict-mode failure is 422.password. (Doc item "password → 422" is obsolete.)V2ExtractResult/V2ExtractMetadata). V2 parse metadata differs from V1's.received_at,created_at— int epoch secscreated_at,completed_at— ISO stringsdata(inline) oroutput_urlresultfailure_reason(string)error {code, message}cancelledcancelled{jobs, org_id, has_more}{jobs, page, page_size, has_more}Even within extract, list uses
failure_reasonbut get useserror{code,message}./v1/files: singlefilepart in, open{string:string}map out (file_refis a key). Lives on the ADE host./v2/workflow: full third parallel surface (sync + jobs + get + list). Out of scope here, but the resource/waiter machinery accepts it later with no rework.cancelledstatus. Sync routes can 504 on timeout (extract 504 ⇒ workflow cancelled; retry w/ sameidempotency_keyrestarts).Design decisions
Jobshape (chosen): one normalized model for parse + extract —status(common enum),created_at/completed_at(datetime, both int-epoch and ISO normalized via the existingparse_datetime),progress(float),result(typed:V2ParseResponse|V2ExtractResult),error(JobError), plus a.rawescape hatch for envelope-specific fields (org_id,output_url,version). Stable if aide unifies the API later._prepare_urlpasses absolute URLs through untouched, so v2 resources build absolute URLs against a resolved_v2_base_urland inherit auth/retries/http client./v1/filesroutes to the ADE host despite its/v1path.LANDINGAI_ADE_ENVIRONMENTenv var; explicitbase_url/v2_base_urloverrides; if onlybase_urlis set, V2 follows it (one mock captures everything)._client.py(env map,v2_base_urlresolution, onev2cached property per client, widenedenvironmentLiteral) + two re-exports. All other V2 code is new files undertypes/v2/,resources/v2/,lib/. Holds whether or not the Stainless exit ("Problem 1") has landed.v2.extracttakes a pydantic model class, a dict, or a JSON string (extendslib/schema_utils.py), sent as a JSON object in the request body.Environment matrix
https://api.va.landing.aihttps://api.ade.landing.aihttps://api.va.eu-west-1.landing.aihttps://api.ade.eu-west-1.landing.aihttps://api.va.staging.landing.aihttps://api.ade.staging.landing.aihttps://api.va.dev.landing.aihttps://api.ade.dev.landing.aiTask breakdown (13 TDD tasks)
Each task ends with an independently testable deliverable and a commit. Full code in the plan doc.
_client.py(sync+async): 4-pair map,v2_base_urlparam,LANDINGAI_ADE_ENVIRONMENT/LANDINGAI_ADE_V2_BASE_URL,_v2_base_urlattr, widenedenvironmentLiteral,copy(). Tests:tests/test_v2_environment.py.types/v2/{job,parse_response,extract_response,file_upload_response}.py:JobStatus/JobError/Job,V2ParseResponse+metadata+billing,V2ExtractResult+V2ExtractMetadata,V2FileUploadResponse. Tests:tests/test_v2_types.py.resources/v2/_normalize.py:normalize_parse_job,normalize_extract_job(epoch vs ISO,data/result,failure_reason/error). Tests:tests/test_v2_normalize.py.lib/schema_utils.py:coerce_schema_to_dict(pydantic|dict|str) -> dict. Tests:tests/test_v2_schema.py.lib/v2_errors.py:V2SyncTimeoutError,JobWaitTimeoutError,JobFailedError,raise_if_sync_timeout. Tests:tests/test_v2_errors.py.v2sub-client + routing + waiter —resources/v2/{_base,v2}.py:V2ResourceMixin._v2_url,poll_until_terminal/apoll_until_terminal(injected clock/sleep),V2Resource/AsyncV2Resource,client.v2cached properties. Tests: routing via respx.client.v2.files.upload—resources/v2/files.py→ returnsfile_ref, hits ADE host. Tests:tests/api_resources/v2/test_files.py.client.v2.parse(sync) —resources/v2/parse.py:optionsJSON-encoded, 206 returns response withfailed_pages, 504 →V2SyncTimeoutError,save_to. Tests:tests/api_resources/v2/test_parse.py.client.v2.parse_jobs— create/list/get/waitreturning normalizedJob;JobListcarrieshas_more/org_id;priority,output_save_url. Tests appended.client.v2.extract(sync) —resources/v2/extract.py: JSON body, schema coercion,idempotency_key,options.strict, 504 handling,save_to. Tests:tests/api_resources/v2/test_extract.py.client.v2.extract_jobs— create/list/get/wait; mapserror{code,message}, page/page_size envelope. Tests appended.rye run pytest -q;rye run lint. Tests:tests/api_resources/v2/test_async_smoke.py.api.mdV2 section, README V2 subsection,examples/v2_parse.py,examples/v2_extract.py.Open items for the aide team (non-blocking)
idempotency_key: absent from parse routes though present on extract — confirm intent.Jobshape unaffected)./v2/workflowscope confirmation.V2ParseResponseis permissive until the gateway publishes a typed schema.🤖 Generated with Claude Code