Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Loading