Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ from landingai_ade.types.v2 import (
Job,
JobError,
JobStatus,
V2ExtractBilling,
V2ExtractMetadata,
V2ExtractResult,
V2FileUploadResponse,
Expand All @@ -84,7 +85,7 @@ from landingai_ade.types.v2 import (

- <code><a href="./src/landingai_ade/types/v2/job.py">Job</a></code> -- unified job shape: `job_id`, `status` (<code><a href="./src/landingai_ade/types/v2/job.py">JobStatus</a></code>: `pending` / `processing` / `completed` / `failed` / `cancelled`), `created_at`, `completed_at`, `progress`, `result` (a `V2ParseResponse` for parse jobs, a `V2ExtractResult` for extract jobs, or `None` until completion), `error` (<code><a href="./src/landingai_ade/types/v2/job.py">JobError</a></code>), `raw` (the full original envelope as a `dict`), and the `.is_terminal` property.
- <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseResponse</a></code> -- `markdown`, `structure`, `grounding`, `metadata` (<code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseMetadata</a></code>, which nests <code><a href="./src/landingai_ade/types/v2/parse_response.py">V2ParseBilling</a></code>). Loosely typed pending a published gateway schema; unknown fields are retained.
- <code><a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractResult</a></code> -- `extraction`, `extraction_metadata`, `markdown`, `metadata` (<code><a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractMetadata</a></code>).
- <code><a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractResult</a></code> -- `extraction`, `extraction_metadata`, `markdown`, `metadata` (<code><a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractMetadata</a></code>, which nests <code><a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractBilling</a></code>).
- <code><a href="./src/landingai_ade/types/v2/file_upload_response.py">V2FileUploadResponse</a></code> -- `file_ref`.

Methods:
Expand All @@ -93,7 +94,7 @@ Methods:

Synchronous parse. Provide exactly one of `document` (file) or `document_url`. Returns a `V2ParseResponse` on both full success (HTTP 200) and partial success (HTTP 206, where `result.metadata.failed_pages` lists unparsed pages). Raises `V2SyncTimeoutError` (from `landingai_ade.lib.v2_errors`) on a 504; use `parse_jobs` for long-running documents.

- <code title="post /v2/parse/jobs">client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">create</a>(\*, document=..., document_url=..., model=..., options=..., password=..., output_save_url=..., priority=...) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="post /v2/parse/jobs">client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">create</a>(\*, document=..., document_url=..., model=..., options=..., password=..., output_save_url=..., service_tier=...) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/parse/jobs/{job_id}">client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">get</a>(job_id) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/parse/jobs">client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">list</a>(\*, page=..., page_size=..., status=...) -> JobList[<a href="./src/landingai_ade/types/v2/job.py">Job</a>]</code>
- <code>client.v2.parse_jobs.<a href="./src/landingai_ade/resources/v2/parse.py">wait</a>(job_id, \*, timeout=600, poll_interval=None, raise_on_failure=False) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
Expand All @@ -104,7 +105,7 @@ Methods:

Synchronous extract. `schema` accepts a pydantic `BaseModel` subclass, a `dict`, or a JSON-encoded string -- all are coerced to a JSON Schema object. Provide exactly one of `markdown`, `markdown_ref` (e.g. from `client.v2.files.upload`), or `markdown_url`. `strict=True` rejects schemas with unsupported fields (HTTP 422) instead of silently pruning them. Raises `V2SyncTimeoutError` on a 504; use `extract_jobs` for long-running documents.

- <code title="post /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">create</a>(\*, schema, markdown=..., markdown_ref=..., markdown_url=..., model=..., strict=..., idempotency_key=..., priority=...) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="post /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">create</a>(\*, schema, markdown=..., markdown_ref=..., markdown_url=..., model=..., strict=..., idempotency_key=..., service_tier=...) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/extract/jobs/{job_id}">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">get</a>(job_id) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
- <code title="get /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">list</a>(\*, page=..., page_size=..., status=...) -> JobList[<a href="./src/landingai_ade/types/v2/job.py">Job</a>]</code>
- <code>client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">wait</a>(job_id, \*, timeout=600, poll_interval=None, raise_on_failure=False) -> <a href="./src/landingai_ade/types/v2/job.py">Job</a></code>
Expand Down
2 changes: 1 addition & 1 deletion examples/v2_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
document_path = Path(__file__).parent / "sample.pdf"

# Create an async parse job for a (potentially large) document.
job = client.v2.parse_jobs.create(document=document_path, priority="priority")
job = client.v2.parse_jobs.create(document=document_path, service_tier="priority")
print(f"Created parse job {job.job_id} (status={job.status})")

# Block until the job reaches a terminal state.
Expand Down
14 changes: 7 additions & 7 deletions src/landingai_ade/resources/v2/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def _build_extract_body(
model: object,
strict: object,
idempotency_key: object,
priority: object = omit,
service_tier: object = omit,
) -> Dict[str, Any]:
body: Dict[str, Any] = {"schema": coerce_schema_to_dict(schema)}
for key, value in (
Expand All @@ -39,7 +39,7 @@ def _build_extract_body(
("markdown_url", markdown_url),
("model", model),
("idempotency_key", idempotency_key),
("priority", priority),
("service_tier", service_tier),
):
if value is not omit and value is not None:
body[key] = value
Expand Down Expand Up @@ -180,7 +180,7 @@ def create(
model: Optional[str] | Omit = omit,
strict: Optional[bool] | Omit = omit,
idempotency_key: Optional[str] | Omit = omit,
priority: Optional[Literal["standard", "priority"]] | Omit = omit,
service_tier: Optional[Literal["standard", "priority"]] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand Down Expand Up @@ -214,7 +214,7 @@ def create(

idempotency_key: An idempotency key for the request.

priority: Processing priority for the job.
service_tier: Service tier for the job: ``standard`` or ``priority``.

extra_headers: Send extra headers

Expand All @@ -225,7 +225,7 @@ def create(
timeout: Override the client-level default timeout for this request, in seconds
"""
body = _build_extract_body(
schema, markdown, markdown_ref, markdown_url, model, strict, idempotency_key, priority
schema, markdown, markdown_ref, markdown_url, model, strict, idempotency_key, service_tier
)
raw = self._post(
self._v2_url("/v2/extract/jobs"),
Expand Down Expand Up @@ -330,7 +330,7 @@ async def create(
model: Optional[str] | Omit = omit,
strict: Optional[bool] | Omit = omit,
idempotency_key: Optional[str] | Omit = omit,
priority: Optional[Literal["standard", "priority"]] | Omit = omit,
service_tier: Optional[Literal["standard", "priority"]] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand All @@ -340,7 +340,7 @@ async def create(
) -> Job:
"""Async mirror of `ExtractJobsResource.create`. See there for full documentation."""
body = _build_extract_body(
schema, markdown, markdown_ref, markdown_url, model, strict, idempotency_key, priority
schema, markdown, markdown_ref, markdown_url, model, strict, idempotency_key, service_tier
)
raw = await self._post(
self._v2_url("/v2/extract/jobs"),
Expand Down
14 changes: 7 additions & 7 deletions src/landingai_ade/resources/v2/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def create(
options: Optional[Mapping[str, object]] | Omit = omit,
password: Optional[str] | Omit = omit,
output_save_url: Optional[str] | Omit = omit,
priority: Optional[Literal["standard", "priority"]] | Omit = omit,
service_tier: Optional[Literal["standard", "priority"]] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand Down Expand Up @@ -220,7 +220,7 @@ def create(
output_save_url: If zero data retention (ZDR) is enabled, a URL the parsed output should be
saved to instead of being returned in the job result.

priority: Processing priority for the job.
service_tier: Service tier for the job: ``standard`` or ``priority``.

extra_headers: Send extra headers

Expand All @@ -233,8 +233,8 @@ def create(
body = _build_parse_body(document, document_url, model, options, password)
if is_given(output_save_url) and output_save_url is not None:
body["output_save_url"] = output_save_url
if is_given(priority) and priority is not None:
body["priority"] = priority
if is_given(service_tier) and service_tier is not None:
body["service_tier"] = service_tier
body = deepcopy_with_paths(body, [["document"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["document"]])
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
Expand Down Expand Up @@ -340,7 +340,7 @@ async def create(
options: Optional[Mapping[str, object]] | Omit = omit,
password: Optional[str] | Omit = omit,
output_save_url: Optional[str] | Omit = omit,
priority: Optional[Literal["standard", "priority"]] | Omit = omit,
service_tier: Optional[Literal["standard", "priority"]] | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand All @@ -352,8 +352,8 @@ async def create(
body = _build_parse_body(document, document_url, model, options, password)
if is_given(output_save_url) and output_save_url is not None:
body["output_save_url"] = output_save_url
if is_given(priority) and priority is not None:
body["priority"] = priority
if is_given(service_tier) and service_tier is not None:
body["service_tier"] = service_tier
body = deepcopy_with_paths(body, [["document"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["document"]])
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
Expand Down
1 change: 1 addition & 0 deletions src/landingai_ade/types/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
)
from .extract_response import (
V2ExtractResult as V2ExtractResult,
V2ExtractBilling as V2ExtractBilling,
V2ExtractMetadata as V2ExtractMetadata,
)
from .file_upload_response import V2FileUploadResponse as V2FileUploadResponse
10 changes: 9 additions & 1 deletion src/landingai_ade/types/v2/extract_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@

from ..._models import BaseModel

__all__ = ["V2ExtractMetadata", "V2ExtractResult"]
__all__ = ["V2ExtractBilling", "V2ExtractMetadata", "V2ExtractResult"]


class V2ExtractBilling(BaseModel):
"""Billing summary: the service tier the request ran in and the credits charged."""

service_tier: Optional[str] = None
total_credits: Optional[float] = None


class V2ExtractMetadata(BaseModel):
Expand All @@ -13,6 +20,7 @@ class V2ExtractMetadata(BaseModel):
duration_ms: int
doc_id: Optional[str] = None
credit_usage: float = 0.0
billing: Optional[V2ExtractBilling] = None


class V2ExtractResult(BaseModel):
Expand Down
33 changes: 32 additions & 1 deletion tests/api_resources/v2/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_extract_job_create_and_get() -> None:
respx.post("https://api.ade.landing.ai/v2/extract/jobs").mock(
return_value=httpx.Response(202, json={"job_id": "e1", "status": "pending", "created_at": "2026-01-01T00:00:00Z"})
)
job = client.v2.extract_jobs.create(schema={"type": "object"}, markdown="x", priority="priority")
job = client.v2.extract_jobs.create(schema={"type": "object"}, markdown="x", service_tier="priority")
assert job.job_id == "e1" and job.status is JobStatus.PENDING

respx.get("https://api.ade.landing.ai/v2/extract/jobs/e1").mock(
Expand All @@ -82,6 +82,37 @@ def test_extract_job_create_and_get() -> None:
assert done.result.metadata.version == "extract-1"


def test_extract_job_create_sends_service_tier(monkeypatch: pytest.MonkeyPatch) -> None:
# The async job create body must carry `service_tier` (renamed from the
# old `priority` field per the V2 spec).
client = LandingAIADE(apikey=APIKEY)
captured: Dict[str, Any] = {}

def fake_post(path: str, *, cast_to: Any, body: Any = None, options: Any = None, **kwargs: Any) -> Any: # noqa: ARG001
captured["body"] = body
return {"job_id": "e1", "status": "pending"}

monkeypatch.setattr(client.v2.extract_jobs, "_post", fake_post)
client.v2.extract_jobs.create(schema={"type": "object"}, markdown="x", service_tier="priority")
assert captured["body"]["service_tier"] == "priority"
assert "priority" not in captured["body"]


@respx.mock
def test_extract_sync_parses_billing_metadata() -> None:
# The V2 spec adds a `billing` object to the response metadata.
client = LandingAIADE(apikey=APIKEY)
body: Dict[str, Any] = dict(EXTRACT_BODY)
metadata: Dict[str, Any] = dict(EXTRACT_BODY["metadata"])
metadata["billing"] = {"service_tier": "priority", "total_credits": 12.5}
body["metadata"] = metadata
respx.post("https://api.ade.landing.ai/v2/extract").mock(return_value=httpx.Response(200, json=body))
result = client.v2.extract(schema={"type": "object"}, markdown="x")
assert result.metadata.billing is not None
assert result.metadata.billing.service_tier == "priority"
assert result.metadata.billing.total_credits == 12.5


@respx.mock
def test_extract_job_get_failed_maps_error_object() -> None:
client = LandingAIADE(apikey=APIKEY)
Expand Down
53 changes: 50 additions & 3 deletions tests/api_resources/v2/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def test_parse_sync_omits_explicit_none_fields_from_multipart_body() -> None:
def test_parse_job_create_omits_explicit_none_extra_fields(monkeypatch: pytest.MonkeyPatch) -> None:
# Regression test at the body-dict level (before multipart encoding, which
# happens to drop bare `None` values and would otherwise mask this bug):
# `output_save_url=None`/`priority=None` must not survive into the body
# `output_save_url=None`/`service_tier=None` must not survive into the body
# dict handed to the request layer.
client = LandingAIADE(apikey=APIKEY)
captured: Dict[str, Any] = {}
Expand All @@ -84,8 +84,24 @@ def fake_post(path: str, *, cast_to: Any, body: Any = None, files: Any = None, o
return {"job_id": "p1", "status": "pending"}

monkeypatch.setattr(client.v2.parse_jobs, "_post", fake_post)
client.v2.parse_jobs.create(document=b"x", output_save_url=None, priority=None)
client.v2.parse_jobs.create(document=b"x", output_save_url=None, service_tier=None)
assert "output_save_url" not in captured["body"]
assert "service_tier" not in captured["body"]


def test_parse_job_create_sends_service_tier(monkeypatch: pytest.MonkeyPatch) -> None:
# The async job create body must carry `service_tier` (renamed from the
# old `priority` field per the V2 spec).
client = LandingAIADE(apikey=APIKEY)
captured: Dict[str, Any] = {}

def fake_post(path: str, *, cast_to: Any, body: Any = None, files: Any = None, options: Any = None) -> Any: # noqa: ARG001
captured["body"] = body
return {"job_id": "p1", "status": "pending"}

monkeypatch.setattr(client.v2.parse_jobs, "_post", fake_post)
client.v2.parse_jobs.create(document=b"x", service_tier="priority")
assert captured["body"]["service_tier"] == "priority"
assert "priority" not in captured["body"]


Expand Down Expand Up @@ -178,7 +194,7 @@ def test_parse_job_create_normalizes_envelope() -> None:
respx.post("https://api.ade.landing.ai/v2/parse/jobs").mock(
return_value=httpx.Response(202, json={"job_id": "p1", "status": "pending", "received_at": 1700000000})
)
job = client.v2.parse_jobs.create(document=b"pdf", priority="priority")
job = client.v2.parse_jobs.create(document=b"pdf", service_tier="priority")
assert isinstance(job, Job)
assert job.job_id == "p1" and job.status is JobStatus.PENDING

Expand All @@ -198,6 +214,37 @@ def test_parse_job_get_completed_has_typed_result() -> None:
assert job.result.markdown == "# Hello"


@respx.mock
def test_parse_job_get_206_partial_returns_result() -> None:
# Per the V2 spec, GET /v2/parse/jobs/{id} can return 206 (partial success).
# 206 is a 2xx, so the base client treats it as success and the envelope is
# normalized like any completed job.
client = LandingAIADE(apikey=APIKEY)
body: Dict[str, Any] = dict(PARSE_BODY)
metadata: Dict[str, Any] = dict(PARSE_BODY["metadata"])
metadata["failed_pages"] = [2]
body["metadata"] = metadata
respx.get("https://api.ade.landing.ai/v2/parse/jobs/p1").mock(
return_value=httpx.Response(206, json={"job_id": "p1", "status": "completed", "data": body})
)
job = client.v2.parse_jobs.get("p1")
assert job.status is JobStatus.COMPLETED
assert isinstance(job.result, V2ParseResponse)
assert job.result.metadata is not None and job.result.metadata.failed_pages == [2]


@respx.mock
def test_parse_job_get_404_raises_not_found() -> None:
from landingai_ade import NotFoundError

client = LandingAIADE(apikey=APIKEY, max_retries=0)
respx.get("https://api.ade.landing.ai/v2/parse/jobs/missing").mock(
return_value=httpx.Response(404, json={"detail": "not found"})
)
with pytest.raises(NotFoundError):
client.v2.parse_jobs.get("missing")


@respx.mock
def test_parse_job_get_empty_job_id_raises() -> None:
client = LandingAIADE(apikey=APIKEY)
Expand Down