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
16 changes: 1 addition & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ from landingai_ade import LandingAIADE

client = LandingAIADE()

job = client.v2.parse_jobs.create(document=Path("path/to/large_file.pdf"), priority="priority")
job = client.v2.parse_jobs.create(document=Path("path/to/large_file.pdf"), service_tier="priority")
print(job.job_id, job.status)

# Block until the job is terminal (raises JobWaitTimeoutError / JobFailedError on failure paths)
Expand All @@ -302,20 +302,6 @@ if done.result is not None:

`parse_jobs.create` and `extract_jobs.create` both return a normalized `Job` -- one shape shared by parse and extract jobs, even though their upstream envelopes differ. Use `job.raw` to reach any field not surfaced on the typed model.

### Uploading a file for `markdown_ref`

`client.v2.files.upload` stages bytes on the ADE data plane and returns a `file_ref` you can pass as `markdown_ref` to extract:

```python
from pathlib import Path
from landingai_ade import LandingAIADE

client = LandingAIADE()

file_ref = client.v2.files.upload(file=Path("path/to/file.md"))
result = client.v2.extract(schema={"type": "object", "properties": {}}, markdown_ref=file_ref)
```

`client.v2.parse`, `client.v2.extract`, and their async counterparts also accept `save_to`, with the same auto-naming behavior as the V1 methods above.

The async client mirrors this entire surface: `AsyncLandingAIADE().v2.parse(...)`, `await client.v2.parse_jobs.wait(...)`, etc.
Expand Down
8 changes: 4 additions & 4 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ Methods:

Blocks, polling `.get(job_id)` with exponential backoff, until the job reaches a terminal status. Raises `JobWaitTimeoutError` if `timeout` seconds elapse first, and `JobFailedError` if `raise_on_failure=True` and the terminal job carries an `error` (not simply every `failed`/`cancelled` status).

- <code title="post /v2/extract">client.v2.<a href="./src/landingai_ade/resources/v2/v2.py">extract</a>(\*, schema, markdown=..., markdown_ref=..., markdown_url=..., model=..., strict=..., idempotency_key=..., save_to=...) -> <a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractResult</a></code>
- <code title="post /v2/extract">client.v2.<a href="./src/landingai_ade/resources/v2/v2.py">extract</a>(\*, schema, markdown=..., markdown_url=..., model=..., strict=..., save_to=...) -> <a href="./src/landingai_ade/types/v2/extract_response.py">V2ExtractResult</a></code>

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.
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` 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.
Comment thread
yzld2002 marked this conversation as resolved.

- <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="post /v2/extract/jobs">client.v2.extract_jobs.<a href="./src/landingai_ade/resources/v2/extract.py">create</a>(\*, schema, markdown=..., markdown_url=..., model=..., strict=..., 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 All @@ -121,7 +121,7 @@ Methods:

- <code title="post /v1/files">client.v2.files.<a href="./src/landingai_ade/resources/v2/files.py">upload</a>(\*, file) -> str</code>

Stages a file's bytes on the ADE data plane and returns a `file_ref` string, which can be passed as `markdown_ref` to `client.v2.extract`/`client.v2.extract_jobs.create`. Served on the ADE host under `/v1/files` (not `/v2/...`). Raises `LandingAiadeError` if the response has no `file_ref`.
Stages a file's bytes on the ADE data plane and returns a `file_ref` string for use in job inputs. Served on the ADE host under `/v1/files` (not `/v2/...`). Raises `LandingAiadeError` if the response has no `file_ref`.

Notes:

Expand Down
37 changes: 5 additions & 32 deletions src/landingai_ade/resources/v2/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,31 @@
def _build_extract_body(
schema: Union[str, Mapping[str, object], Type[BaseModel]],
markdown: object,
markdown_ref: object,
markdown_url: object,
model: object,
strict: object,
idempotency_key: object,
service_tier: object = omit,
) -> Dict[str, Any]:
provided = [
name
for name, value in (
("markdown", markdown),
("markdown_ref", markdown_ref),
("markdown_url", markdown_url),
)
if value is not omit and value is not None
]
if len(provided) != 1:
raise ValueError(
"extract requires exactly one markdown source: provide one of "
"`markdown`, `markdown_ref`, or `markdown_url`"
"`markdown` or `markdown_url`"
Comment thread
yzld2002 marked this conversation as resolved.
+ (f" (received: {', '.join(provided)})" if provided else "")
+ "."
)
body: Dict[str, Any] = {"schema": coerce_schema_to_dict(schema)}
for key, value in (
("markdown", markdown),
("markdown_ref", markdown_ref),
("markdown_url", markdown_url),
("model", model),
("idempotency_key", idempotency_key),
("service_tier", service_tier),
):
if value is not omit and value is not None:
Expand All @@ -70,11 +65,9 @@ def run(
*,
schema: Union[str, Mapping[str, object], Type[BaseModel]],
markdown: Optional[str] | Omit = omit,
markdown_ref: Optional[str] | Omit = omit,
markdown_url: Optional[str] | Omit = omit,
model: Optional[str] | Omit = omit,
strict: Optional[bool] | Omit = omit,
idempotency_key: Optional[str] | Omit = omit,
save_to: str | Path | None = None,
# 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.
Expand All @@ -97,9 +90,6 @@ def run(

markdown: Markdown content to extract data from.

markdown_ref: A reference (e.g. from a prior parse) to markdown content to
extract data from.

markdown_url: The URL to the markdown file to extract data from.

model: The version of the model to use for extraction.
Expand All @@ -108,8 +98,6 @@ def run(
False, prune unsupported fields and continue. Sent as
`options.strict`.

idempotency_key: An idempotency key for the request.

save_to: Optional output path. If a directory, auto-generates the filename
(e.g. {input_file}_extract_output.json, or extract_output.json when no
input filename is available). If a full path ending in .json, saves there
Expand All @@ -123,7 +111,7 @@ def run(

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)
body = _build_extract_body(schema, markdown, markdown_url, model, strict)
try:
result = self._post(
self._v2_url("/v2/extract"),
Expand All @@ -150,11 +138,9 @@ async def run(
*,
schema: Union[str, Mapping[str, object], Type[BaseModel]],
markdown: Optional[str] | Omit = omit,
markdown_ref: Optional[str] | Omit = omit,
markdown_url: Optional[str] | Omit = omit,
model: Optional[str] | Omit = omit,
strict: Optional[bool] | Omit = omit,
idempotency_key: Optional[str] | Omit = omit,
save_to: str | Path | None = None,
# 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.
Expand All @@ -164,7 +150,7 @@ async def run(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> V2ExtractResult:
"""Async mirror of `ExtractResource.run`. See there for full documentation."""
body = _build_extract_body(schema, markdown, markdown_ref, markdown_url, model, strict, idempotency_key)
body = _build_extract_body(schema, markdown, markdown_url, model, strict)
try:
result = await self._post(
self._v2_url("/v2/extract"),
Expand All @@ -191,11 +177,9 @@ def create(
*,
schema: Union[str, Mapping[str, object], Type[BaseModel]],
markdown: Optional[str] | Omit = omit,
markdown_ref: Optional[str] | Omit = omit,
markdown_url: Optional[str] | Omit = omit,
model: Optional[str] | Omit = omit,
strict: Optional[bool] | Omit = omit,
idempotency_key: Optional[str] | 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.
Expand All @@ -217,9 +201,6 @@ def create(

markdown: Markdown content to extract data from.

markdown_ref: A reference (e.g. from a prior parse) to markdown content to
extract data from.

markdown_url: The URL to the markdown file to extract data from.

model: The version of the model to use for extraction.
Expand All @@ -228,8 +209,6 @@ def create(
False, prune unsupported fields and continue. Sent as
`options.strict`.

idempotency_key: An idempotency key for the request.

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

extra_headers: Send extra headers
Expand All @@ -240,9 +219,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, service_tier
)
body = _build_extract_body(schema, markdown, markdown_url, model, strict, service_tier)
raw = self._post(
self._v2_url("/v2/extract/jobs"),
body=body,
Expand Down Expand Up @@ -341,11 +318,9 @@ async def create(
*,
schema: Union[str, Mapping[str, object], Type[BaseModel]],
markdown: Optional[str] | Omit = omit,
markdown_ref: Optional[str] | Omit = omit,
markdown_url: Optional[str] | Omit = omit,
model: Optional[str] | Omit = omit,
strict: Optional[bool] | Omit = omit,
idempotency_key: Optional[str] | 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.
Expand All @@ -355,9 +330,7 @@ async def create(
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> 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, service_tier
)
body = _build_extract_body(schema, markdown, markdown_url, model, strict, service_tier)
raw = await self._post(
self._v2_url("/v2/extract/jobs"),
body=body,
Expand Down
4 changes: 2 additions & 2 deletions src/landingai_ade/resources/v2/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def upload(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> str:
"""Stage bytes on the data plane; returns a `file_ref` for use as extract `markdown_ref`."""
"""Stage bytes on the data plane; returns a `file_ref` for use in job inputs."""
body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
Expand Down Expand Up @@ -55,7 +55,7 @@ async def upload(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> str:
"""Stage bytes on the data plane; returns a `file_ref` for use as extract `markdown_ref`."""
"""Stage bytes on the data plane; returns a `file_ref` for use in job inputs."""
body = deepcopy_with_paths({"file": file}, [["file"]])
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
Expand Down
8 changes: 0 additions & 8 deletions src/landingai_ade/resources/v2/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,9 @@ def extract(
*,
schema: Union[str, Mapping[str, object], Type[BaseModel]],
markdown: Optional[str] | Omit = omit,
markdown_ref: Optional[str] | Omit = omit,
markdown_url: Optional[str] | Omit = omit,
model: Optional[str] | Omit = omit,
strict: Optional[bool] | Omit = omit,
idempotency_key: Optional[str] | Omit = omit,
save_to: str | Path | None = None,
# 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.
Expand All @@ -113,11 +111,9 @@ def extract(
return self._extract.run(
schema=schema,
markdown=markdown,
markdown_ref=markdown_ref,
markdown_url=markdown_url,
model=model,
strict=strict,
idempotency_key=idempotency_key,
save_to=save_to,
extra_headers=extra_headers,
extra_query=extra_query,
Expand Down Expand Up @@ -194,11 +190,9 @@ async def extract(
*,
schema: Union[str, Mapping[str, object], Type[BaseModel]],
markdown: Optional[str] | Omit = omit,
markdown_ref: Optional[str] | Omit = omit,
markdown_url: Optional[str] | Omit = omit,
model: Optional[str] | Omit = omit,
strict: Optional[bool] | Omit = omit,
idempotency_key: Optional[str] | Omit = omit,
save_to: str | Path | None = None,
# 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.
Expand All @@ -211,11 +205,9 @@ async def extract(
return await self._extract.run(
schema=schema,
markdown=markdown,
markdown_ref=markdown_ref,
markdown_url=markdown_url,
model=model,
strict=strict,
idempotency_key=idempotency_key,
save_to=save_to,
extra_headers=extra_headers,
extra_query=extra_query,
Expand Down
5 changes: 2 additions & 3 deletions tests/api_resources/v2/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ def test_extract_sync_json_body_with_pydantic_schema() -> None:
route = respx.post("https://api.ade.landing.ai/v2/extract").mock(
return_value=httpx.Response(200, json=EXTRACT_BODY)
)
result = client.v2.extract(schema=Invoice, markdown="# doc", idempotency_key="k1")
result = client.v2.extract(schema=Invoice, markdown="# doc")
assert isinstance(result, V2ExtractResult) and result.metadata.version == "extract-1"
req = json.loads(route.calls.last.request.content)
assert req["schema"]["type"] == "object" and "revenue" in req["schema"]["properties"]
assert req["markdown"] == "# doc"
assert req["idempotency_key"] == "k1"
assert route.calls.last.request.headers["content-type"].startswith("application/json")


Expand All @@ -55,7 +54,7 @@ def test_extract_sync_strict_option() -> None:
@respx.mock
def test_extract_requires_a_markdown_source() -> None:
# api.md documents the contract: provide exactly one of markdown /
# markdown_ref / markdown_url. Omitting all three used to send a sourceless
# markdown_url. Omitting both used to send a sourceless
# body and surface an opaque server 500; the SDK now fails fast client-side.
# No route is registered: @respx.mock keeps this hermetic, so a regression in
# the guard fails loudly on an unmocked request instead of hitting the network.
Expand Down