From 9debc2781438651f1727cdd52da3d72800fc2166 Mon Sep 17 00:00:00 2001 From: Bianca Ragsdale <128001048+lai-bianca@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:16:18 -0700 Subject: [PATCH 1/4] docs: rewrite README to lead with the v2 API Restructure the README around the v2 (DPT-3) surface so new users and AI agents reach the current API first: - Quickstart shows the core parse -> extract flow in one script - Dedicated Parse, Extract, and async jobs sections with response field tables, service_tier, and wait() semantics - New limits table (file types, 50 MiB, 100 pages) - v1 condensed to a single section with a method table and split example; full reference remains in api.md - Update playground and API-key links to ade.landing.ai - Lowercase v1/v2 in prose Remove the MCP Server section: landingai-ade-mcp is generated by the Stainless toolchain, which sunsets on 2026-09-01, so the server will no longer be regenerated. Removing the recommendation until its future is decided (it also only covers the v1 API today). Co-Authored-By: Claude Fable 5 --- README.md | 620 ++++++++++++++++-------------------------------------- 1 file changed, 178 insertions(+), 442 deletions(-) diff --git a/README.md b/README.md index 1edc5ce..3d74b26 100644 --- a/README.md +++ b/README.md @@ -14,414 +14,257 @@ ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue) [![License](https://img.shields.io/pypi/l/landingai-ade)](https://pypi.org/project/landingai-ade/) - -**[Playground](https://va.landing.ai) · [Discord](https://discord.com/invite/RVcW3j9RgR) · [Blog](https://landing.ai/blog) · [Docs](https://docs.landing.ai)** +**[Docs](https://docs.landing.ai) · [Playground](https://ade.landing.ai) · [LandingAI](https://landing.ai)** +The official Python library for the [LandingAI Agentic Document Extraction (ADE) API](https://ade.landing.ai). Parse PDFs and images into structured, grounded Markdown, then extract typed fields with a JSON Schema or Pydantic model. -A Python library for interacting with the **LandingAI Agentic Document Extraction REST API**, designed for flexibility, reliability, clarity, and performance. Built for Python 3.9+. - - -## ✨ Features - -* ✅ Fully-typed SDK with Pydantic response models -* ⚡️ Sync & Async clients -* 📄 Large document processing via async jobs -* 🔁 Built-in retries with exponential backoff -* 🔐 Secure API key handling -* 📦 Seamless file uploads -* 🧩 Schema-based data extraction -* 🔌 Pluggable HTTP backends (`httpx` or `aiohttp`) -* 💾 Optional `save_to` parameter to save responses to a folder with auto-generated filenames - -## MCP Server - -Use the LandingAI ADE MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. - -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=landingai-ade-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImxhbmRpbmdhaS1hZGUtbWNwIl0sImVudiI6eyJWSVNJT05fQUdFTlRfQVBJX0tFWSI6Ik15IEFwaWtleSJ9fQ) - -> Note: You may need to set environment variables in your MCP client. - -## Documentation - -The REST API documentation can be found on [docs.landing.ai](https://docs.landing.ai/). The full API of this library can be found in [api.md](api.md). +- Fully typed requests and Pydantic response models +- Sync and async clients with identical surfaces +- Async jobs with a built-in `wait()` helper for large documents +- Automatic retries with exponential backoff +- Optional `save_to` parameter to write responses to disk ## Installation ```sh -# install from PyPI pip install landingai-ade ``` -## Usage - -The full API of this library can be found in [api.md](api.md). - -### Parse +## Set Your API Key -```python -import os -from pathlib import Path -from landingai_ade import LandingAIADE - -client = LandingAIADE( - apikey=os.environ.get("VISION_AGENT_API_KEY"), # This is the default and can be omitted - # defaults to "production". - environment="eu", -) +[Generate an API key](https://ade.landing.ai/settings/api-key), then export it as an environment variable. The client reads it automatically. -response = client.parse( - # use document= for local files, document_url= for remote URLs - document=Path("path/to/file"), - model="dpt-2-latest", - save_to="./output_folder", # optional: saves as {input_file}_parse_output.json -) -print(response.chunks) +```sh +export VISION_AGENT_API_KEY= ``` -While you can provide a `apikey` keyword argument, -we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `VISION_AGENT_API_KEY="My Apikey"` to your `.env` file -so that your Apikey is not stored in source control. - -### Split - -```python -import json -from pathlib import Path -from landingai_ade import LandingAIADE - -client = LandingAIADE() - -# Parse the document -parse_response = client.parse(document=Path("/path/to/document.pdf"), model="dpt-2-latest") - -# Define Split Rules -split_class = [ - { - "name": "Bank Statement", - "description": "Document from a bank that summarizes all account activity over a period of time.", - }, - { - "name": "Pay Stub", - "description": "Document that details an employee's earnings, deductions, and net pay for a specific pay period.", - "identifier": "Pay Stub Date", - }, -] - -# Split using the Markdown string from parse response -split_response = client.split( - split_class=json.dumps(split_class), - markdown=parse_response.markdown, # Pass Markdown string directly - model="split-latest", -) - -# Access the splits -for split in split_response.splits: - print(f"Classification: {split.classification}") - print(f"Identifier: {split.identifier}") - print(f"Pages: {split.pages}") -``` +You can also pass the key directly with `LandingAIADE(apikey=...)`. To keep keys out of source control, use a tool like [python-dotenv](https://pypi.org/project/python-dotenv/). -### Parse Jobs +## Quickstart -For processing large documents asynchronously: +Parse a document, then extract structured data from it: ```python -import os from pathlib import Path +from pydantic import BaseModel, Field from landingai_ade import LandingAIADE -client = LandingAIADE( - apikey=os.environ.get("VISION_AGENT_API_KEY"), -) +class Invoice(BaseModel): + invoice_number: str = Field(description="The invoice number") + total: str = Field(description="Invoice grand total") -# Create an async parse job -job = client.parse_jobs.create( - document=Path("path/to/large_file.pdf"), - model="dpt-2-latest", -) -print(f"Job created with ID: {job.job_id}") +client = LandingAIADE() # reads VISION_AGENT_API_KEY -# Get job status -job_status = client.parse_jobs.get(job.job_id) -print(f"Status: {job_status.status}") +# 1. Parse: convert the document to structured Markdown +parsed = client.v2.parse(document=Path("invoice.pdf")) +print(parsed.markdown) -# List all jobs (with optional filtering) -response = client.parse_jobs.list( - status="completed", - page=0, - page_size=10, -) -for job in response.jobs: - print(f"Job {job.job_id}: {job.status}") +# 2. Extract: pull typed fields out of the Markdown +result = client.v2.extract(schema=Invoice, markdown=parsed.markdown) +print(result.extraction) ``` -### Extract +Use `client.v2` for new projects. It is the current API, powered by the DPT-3 model family. The earlier v1 methods (`client.parse`, `client.extract`, `client.split`, and others) remain fully supported; see [v1 API](#v1-api). -```python -import os -from pathlib import Path -from landingai_ade import LandingAIADE -from landingai_ade.lib import pydantic_to_json_schema -from pydantic import BaseModel, Field +The full method reference for both APIs is in [api.md](api.md); usage guides are at [docs.landing.ai](https://docs.landing.ai). +## Parse -# Define your schema -class Person(BaseModel): - name: str = Field(description="Person's name") - age: int = Field(description="Person's age") - - -# Convert to JSON schema -schema = pydantic_to_json_schema(Person) -# Use with the SDK -client = LandingAIADE(apikey=os.environ.get("VISION_AGENT_API_KEY")) -response = client.extract( - schema=schema, - # use markdown= for local files, markdown_url= for remote URLs - markdown=Path("path/to/file.md"), - save_to="./output_folder", # optional: saves as {input_file}_extract_output.json -) -``` - -### Extract Jobs - -For extracting structured data from large markdown documents asynchronously: +Use `client.v2.parse` to convert a document into Markdown plus a structure tree and grounding (pixel-coordinate bounding boxes for every element). Provide exactly one of `document` (a local file) or `document_url`. ```python -import os from pathlib import Path from landingai_ade import LandingAIADE -client = LandingAIADE( - apikey=os.environ.get("VISION_AGENT_API_KEY"), -) +client = LandingAIADE() -# Create an async extract job -job = client.extract_jobs.create( - schema='{"type": "object", "properties": {"title": {"type": "string"}}}', - markdown=Path("path/to/large_file.md"), - model="extract-latest", +# Parse a local file +parsed = client.v2.parse( + document=Path("path/to/file.pdf"), + model="dpt-3-pro-latest", # optional; defaults to the latest DPT-3 Pro model + save_to="./output", # optional; saves as {input_file}_parse_output.json ) -print(f"Job created with ID: {job.job_id}") -# Get job status -job_status = client.extract_jobs.get(job.job_id) -print(f"Status: {job_status.status}") +# Or parse a file at a URL +parsed = client.v2.parse(document_url="https://example.com/file.pdf") -# List all jobs (with optional filtering) -response = client.extract_jobs.list( - status="completed", - page=0, - page_size=10, -) -for job in response.jobs: - print(f"Job {job.job_id}: {job.status}") +print(parsed.markdown) # full document as Markdown +print(parsed.metadata.page_count) # pages processed ``` -## V2 API - -`client.v2` is a new, **additive** sub-client for LandingAI's next-generation ADE gateway. It does not replace or change anything about the V1 usage above -- `client.parse`, `client.extract`, `client.parse_jobs`, `client.extract_jobs`, etc. all keep working exactly as documented. Use `client.v2.*` when you want the newer parse/extract surface. +The response is a `V2ParseResponse`: -The V2 gateway lives on its own host (`api.ade.[env].landing.ai`), separate from the V1 host (`api.va.[env].landing.ai`). Select the environment the same way as V1, via the `environment` argument or the `LANDINGAI_ADE_ENVIRONMENT` env var: +| Field | Description | +| --- | --- | +| `markdown` | The full document as one Markdown string, in reading order. | +| `structure` | A typed tree (`document` → pages → elements) with element types and character spans into `markdown`. | +| `grounding` | A tree mirroring `structure` that adds pixel-coordinate bounding boxes for each element. | +| `metadata` | Processing details: `page_count`, `failed_pages`, `duration_ms`, and `billing` (credits used). | -```python -import os -from landingai_ade import LandingAIADE +If some pages cannot be parsed, the request still succeeds (HTTP 206) and `metadata.failed_pages` lists the pages that failed. If a synchronous parse times out, the client raises `V2SyncTimeoutError`; use [jobs](#process-large-documents-asynchronously-jobs) instead. -client = LandingAIADE( - apikey=os.environ.get("VISION_AGENT_API_KEY"), - # one of "production" (default), "eu", "staging", "dev" - # can also be set via the LANDINGAI_ADE_ENVIRONMENT env var instead of passing it here - environment="staging", -) -``` +## Extract -### Sync parse and extract +Use `client.v2.extract` to pull structured fields out of Markdown (typically from a parse response) using a schema. The `schema` parameter accepts a Pydantic `BaseModel` subclass, a `dict`, or a JSON string. Provide exactly one Markdown source: `markdown` or `markdown_url`. ```python -from pathlib import Path +from pydantic import BaseModel, Field from landingai_ade import LandingAIADE +class Person(BaseModel): + name: str = Field(description="Person's full name") + age: int = Field(description="Person's age") + client = LandingAIADE() -parse_result = client.v2.parse( - document=Path("path/to/file.pdf"), - save_to="./output_folder", # optional: saves as {input_file}_parse_output.json +result = client.v2.extract( + schema=Person, # Pydantic model, dict, or JSON string + markdown=parsed.markdown, # or markdown_url="https://example.com/doc.md" + save_to="./output", # optional ) -print(parse_result.markdown) -extract_result = client.v2.extract( - schema='{"type": "object", "properties": {"total": {"type": "string"}}}', - markdown=parse_result.markdown, - strict=False, # prune unsupported schema fields instead of raising a 422 -) -print(extract_result.extraction) +print(result.extraction) # {"name": "...", "age": ...} +print(result.extraction_metadata) # per-field source spans in the Markdown ``` -### Passing a pydantic model as the extract schema +The response is a `V2ExtractResult`: -`schema` accepts a pydantic `BaseModel` subclass directly -- no need to convert it yourself: +| Field | Description | +| --- | --- | +| `extraction` | The extracted values, matching your schema. | +| `extraction_metadata` | Mirrors `extraction`; each field carries the character spans in the Markdown that the value came from. | +| `metadata` | Processing details, including credits used. | -```python -from pydantic import BaseModel, Field -from landingai_ade import LandingAIADE - - -class Invoice(BaseModel): - total: str = Field(description="Invoice grand total") - - -client = LandingAIADE() -result = client.v2.extract(schema=Invoice, markdown_url="https://example.com/doc.md") -print(result.extraction) -``` +By default, unsupported schema fields are pruned and reported. Pass `strict=True` to reject such schemas with an error (HTTP 422) instead. -### Async jobs and `wait()` +## Process Large Documents Asynchronously (Jobs) -For large documents, create a job and poll it, or block until it finishes with `.wait()`: +For documents that take longer than a synchronous request allows, create a job and wait for it. `client.v2.parse_jobs` and `client.v2.extract_jobs` share the same shape: `create`, `get`, `list`, and `wait`. ```python from pathlib import Path from landingai_ade import LandingAIADE +from landingai_ade.lib.v2_errors import JobFailedError, JobWaitTimeoutError client = LandingAIADE() -job = client.v2.parse_jobs.create(document=Path("path/to/large_file.pdf"), service_tier="priority") +job = client.v2.parse_jobs.create( + document=Path("path/to/large_file.pdf"), + service_tier="standard", # "standard" (default, lower cost) or "priority" (faster) +) print(job.job_id, job.status) -# Block until the job is terminal (raises JobWaitTimeoutError / JobFailedError on failure paths) -done = client.v2.parse_jobs.wait(job.job_id, timeout=600, raise_on_failure=True) -if done.result is not None: +# Block until the job finishes (polls with backoff) +try: + done = client.v2.parse_jobs.wait(job.job_id, timeout=600, raise_on_failure=True) print(done.result.markdown[:200]) - -# client.v2.extract_jobs.{create,get,list,wait} mirror the same shape for extract jobs. +except JobWaitTimeoutError: + print("Job did not finish in time; it is still running server-side.") +except JobFailedError as e: + print(f"Job failed: {e}") ``` -`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. +Every job method returns a normalized `Job` with `job_id`, `status` (`pending`, `processing`, `completed`, `failed`, or `cancelled`), `progress`, `result`, `error`, and `raw` (the unmodified API envelope, for any field not surfaced on the typed model). -`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. +```python +# Poll manually instead of blocking +job = client.v2.parse_jobs.get(job.job_id) + +# List jobs, with optional filtering +jobs = client.v2.parse_jobs.list(status="completed", page=0, page_size=10) +for job in jobs: + print(job.job_id, job.status) +print(jobs.has_more) +``` -The async client mirrors this entire surface: `AsyncLandingAIADE().v2.parse(...)`, `await client.v2.parse_jobs.wait(...)`, etc. +Extract jobs work the same way: `client.v2.extract_jobs` accepts the same arguments as `client.v2.extract`. -## Async usage +## Async Client -Simply import `AsyncLandingAIADE` instead of `LandingAIADE` and use `await` with each API call: +Import `AsyncLandingAIADE` and `await` each call. The async client mirrors the entire sync surface, including `client.v2`. ```python -import os import asyncio from pathlib import Path from landingai_ade import AsyncLandingAIADE -client = AsyncLandingAIADE( - apikey=os.environ.get("VISION_AGENT_API_KEY"), # This is the default and can be omitted - # defaults to "production". - environment="eu", -) - - async def main() -> None: - response = await client.parse( - document=Path("path/to/file"), - model="dpt-2-latest", - ) - print(response.chunks) - + async with AsyncLandingAIADE() as client: + parsed = await client.v2.parse(document=Path("path/to/file.pdf")) + print(parsed.markdown) asyncio.run(main()) ``` -Functionality between the synchronous and asynchronous clients is otherwise identical. - -### With aiohttp - -By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. - -You can enable this by installing `aiohttp`: +For higher concurrency, you can use `aiohttp` as the HTTP backend instead of the default `httpx`: ```sh -# install from PyPI pip install landingai-ade[aiohttp] ``` -Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: - ```python -import os -import asyncio -from pathlib import Path -from landingai_ade import DefaultAioHttpClient -from landingai_ade import AsyncLandingAIADE +from landingai_ade import AsyncLandingAIADE, DefaultAioHttpClient - -async def main() -> None: - async with AsyncLandingAIADE( - apikey=os.environ.get("VISION_AGENT_API_KEY"), # This is the default and can be omitted - http_client=DefaultAioHttpClient(), - ) as client: - response = await client.parse( - document=Path("path/to/file"), - model="dpt-2-latest", - ) - print(response.chunks) - - -asyncio.run(main()) +async with AsyncLandingAIADE(http_client=DefaultAioHttpClient()) as client: + ... ``` -## Using types - -Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: +## Environments -- Serializing back into JSON, `model.to_json()` -- Converting to a dictionary, `model.to_dict()` - -Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. - -## Nested params - -Nested parameters are dictionaries, typed using `TypedDict`, for example: +The `environment` argument selects the region. Set it in code or with the `LANDINGAI_ADE_ENVIRONMENT` environment variable. ```python from landingai_ade import LandingAIADE -client = LandingAIADE() - -response = client.parse( - custom_prompts={}, -) -print(response.custom_prompts) +client = LandingAIADE(environment="eu") # "production" (default) or "eu" ``` -## File uploads +API keys are per-environment: an EU key works only with `environment="eu"`. To point the client at a mock server or proxy, pass `base_url` (and `v2_base_url` if v2 traffic needs a separate target) or set the `LANDINGAI_ADE_BASE_URL` environment variable. -Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. +## v1 API + +The v1 methods sit directly on the client. + +| Method | What it does | +| --- | --- | +| `client.parse(...)` | Parse a document with the DPT-2 model family. | +| `client.extract(...)` | Extract fields from Markdown. | +| `client.split(...)` | Split a multi-document file into sub-documents by classification. | +| `client.classify(...)` | Classify each page of a document. | +| `client.section(...)` | Generate a hierarchical table of contents. | +| `client.extract_build_schema(...)` | Generate an extraction schema from sample documents. | +| `client.parse_jobs`, `client.extract_jobs` | Async jobs (`create`, `get`, `list`). | ```python +import json from pathlib import Path from landingai_ade import LandingAIADE client = LandingAIADE() -client.parse( - document=Path("/path/to/file"), +# Split a combined file into sub-documents +parsed = client.parse(document=Path("statements.pdf"), model="dpt-2-latest") +split = client.split( + split_class=json.dumps([ + {"name": "Bank Statement", "description": "Summarizes account activity over a period."}, + {"name": "Pay Stub", "description": "Details an employee's earnings for a pay period."}, + ]), + markdown=parsed.markdown, + model="split-latest", ) +for s in split.splits: + print(s.classification, s.pages) ``` -The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. - -## Handling errors - -When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `landingai_ade.APIConnectionError` is raised. - -When the API returns a non-success status code (that is, 4xx or 5xx -response), a subclass of `landingai_ade.APIStatusError` is raised, containing `status_code` and `response` properties. +## Handling Errors All errors inherit from `landingai_ade.APIError`. +- Connection problems raise a subclass of `landingai_ade.APIConnectionError`. +- Non-success HTTP status codes (4xx, 5xx) raise a subclass of `landingai_ade.APIStatusError` with `status_code` and `response` properties. + ```python import landingai_ade from landingai_ade import LandingAIADE @@ -429,20 +272,17 @@ from landingai_ade import LandingAIADE client = LandingAIADE() try: - client.parse() + client.v2.parse(document_url="https://example.com/file.pdf") except landingai_ade.APIConnectionError as e: print("The server could not be reached") - print(e.__cause__) # an underlying Exception, likely raised within httpx. -except landingai_ade.RateLimitError as e: - print("A 429 status code was received; we should back off a bit.") + print(e.__cause__) +except landingai_ade.RateLimitError: + print("A 429 status code was received; back off and retry.") except landingai_ade.APIStatusError as e: - print("Another non-200-range status code was received") print(e.status_code) print(e.response) ``` -Error codes are as follows: - | Status Code | Error Type | | ----------- | -------------------------- | | 400 | `BadRequestError` | @@ -456,162 +296,65 @@ Error codes are as follows: ### Retries -Certain errors are automatically retried 2 times by default, with a short exponential backoff. -Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, -429 Rate Limit, and >=500 Internal errors are all retried by default. - -You can use the `max_retries` option to configure or disable retry settings: +Connection errors, 408, 409, 429, and 5xx responses are retried twice by default with exponential backoff. Configure with `max_retries`: ```python -from landingai_ade import LandingAIADE - -# Configure the default for all requests: -client = LandingAIADE( - # default is 2 - max_retries=0, -) - -# Or, configure per-request: -client.with_options(max_retries=5).parse() +client = LandingAIADE(max_retries=0) # default is 2 +client.with_options(max_retries=5).v2.parse(...) # per-request ``` ### Timeouts -By default requests time out after 8 minutes. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: +Requests time out after 8 minutes by default. Configure with `timeout` (a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration)): ```python -from landingai_ade import LandingAIADE - -# Configure the default for all requests: -client = LandingAIADE( - # 20 seconds (default is 8 minutes) - timeout=20.0, -) - -# More granular control: -client = LandingAIADE( - timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), -) - -# Override per-request: -client.with_options(timeout=5.0).parse() -``` - -On timeout, an `APITimeoutError` is thrown. - -Note that requests that time out are [retried twice by default](#retries). - -## Advanced - -### Logging - -We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. - -You can enable logging by setting the environment variable `LANDINGAI_ADE_LOG` to `info`. - -```shell -$ export LANDINGAI_ADE_LOG=info +client = LandingAIADE(timeout=20.0) # seconds +client.with_options(timeout=5.0).v2.parse(...) # per-request ``` -Or to `debug` for more verbose logging. - -### How to tell whether `None` means `null` or missing - -In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: +On timeout, an `APITimeoutError` is raised. Timed-out requests are retried twice by default. -```py -if response.my_field is None: - if 'my_field' not in response.model_fields_set: - print('Got json like {}, without a "my_field" key present at all.') - else: - print('Got json like {"my_field": null}.') -``` +## Advanced Usage ### Accessing raw response data (e.g. headers) -The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., - -```py -from landingai_ade import LandingAIADE - -client = LandingAIADE() -response = client.with_raw_response.parse() -print(response.headers.get('X-My-Header')) - -client = response.parse() # get the object that `parse()` would have returned -print(client.chunks) -``` - -These methods return an [`APIResponse`](https://github.com/landing-ai/ade-python/tree/main/src/landingai_ade/_response.py) object. - -The async client returns an [`AsyncAPIResponse`](https://github.com/landing-ai/ade-python/tree/main/src/landingai_ade/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. - -#### `.with_streaming_response` - -The above interface eagerly reads the full response body when you make the request, which may not always be what you want. - -To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. +Prefix any method call with `.with_raw_response.` to get the raw HTTP response: ```python -with client.with_streaming_response.parse() as response: - print(response.headers.get("X-My-Header")) - - for line in response.iter_lines(): - print(line) +response = client.with_raw_response.parse(document=Path("file.pdf"), model="dpt-2-latest") +print(response.headers.get("X-My-Header")) +parsed = response.parse() # the object the method would have returned ``` -The context manager is required so that the response will reliably be closed. - -### Making custom/undocumented requests +Use `.with_streaming_response` instead to stream the body rather than reading it eagerly; it requires a context manager and reads the body only when you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()`, or `.parse()`. These return [`APIResponse`](https://github.com/landing-ai/ade-python/tree/main/src/landingai_ade/_response.py) (or `AsyncAPIResponse`) objects. -This library is typed for convenient access to the documented API. +### Nested params and file uploads -If you need to access undocumented endpoints, params, or response properties, the library can still be used. +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict); responses are [Pydantic models](https://docs.pydantic.dev) with helpers such as `model.to_json()` and `model.to_dict()`. File upload parameters accept `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, or a `(filename, contents, media type)` tuple; the async client reads `PathLike` files asynchronously. -#### Undocumented endpoints - -To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other -http verbs. Options on the client will be respected (such as retries) when making this request. - -```py -import httpx - -response = client.post( - "/foo", - cast_to=httpx.Response, - body={"my_param": True}, -) +### How to tell whether `None` means `null` or missing -print(response.headers.get("x-foo")) +```python +if response.my_field is None: + if "my_field" not in response.model_fields_set: + print('The "my_field" key was absent from the response.') + else: + print('The response contained "my_field": null.') ``` -#### Undocumented request params - -If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request -options. +### Making custom or undocumented requests -#### Undocumented response properties - -To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You -can also get all the extra fields on the Pydantic model as a dict with -[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). +Use `client.get` / `client.post` for undocumented endpoints (client options such as retries still apply), and `extra_query`, `extra_body`, or `extra_headers` for undocumented parameters. Undocumented response properties are available via `response.unknown_prop` or [`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). ### Configuring the HTTP client -You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: - -- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) -- Custom [transports](https://www.python-httpx.org/advanced/transports/) -- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality +Override the [httpx client](https://www.python-httpx.org/api/#client) for proxies, custom transports, or other advanced behavior: ```python import httpx from landingai_ade import LandingAIADE, DefaultHttpxClient client = LandingAIADE( - # Or use the `LANDINGAI_ADE_BASE_URL` env var - base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), @@ -619,24 +362,23 @@ client = LandingAIADE( ) ``` -You can also customize the client on a per-request basis by using `with_options()`: - -```python -client.with_options(http_client=DefaultHttpxClient(...)) -``` +You can also change it per-request with `client.with_options(http_client=...)`. ### Managing HTTP resources -By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. - -```py -from landingai_ade import LandingAIADE +The client closes HTTP connections when garbage collected. Close it explicitly with `.close()`, or use a context manager: +```python with LandingAIADE() as client: - # make requests here - ... + ... # connections close on exit +``` + +### Logging + +Set the `LANDINGAI_ADE_LOG` environment variable to `info` (or `debug` for more detail): -# HTTP client is now closed +```sh +export LANDINGAI_ADE_LOG=info ``` ## Versioning @@ -649,15 +391,9 @@ This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) con We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. -We are keen for your feedback; please open an [issue](https://www.github.com/landing-ai/ade-python/issues) with questions, bugs, or suggestions. - -### Determining the installed version - -If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. +To check the version in use at runtime: -You can determine the version that is being used at runtime with: - -```py +```python import landingai_ade print(landingai_ade.__version__) ``` @@ -668,4 +404,4 @@ Python 3.9 or higher. ## Contributing -See [the contributing documentation](./CONTRIBUTING.md). +See [the contributing documentation](./CONTRIBUTING.md). We welcome [issues](https://www.github.com/landing-ai/ade-python/issues) with questions, bugs, or suggestions. From ea4f32a22d6cff0536fc30da2f84dbc18786240d Mon Sep 17 00:00:00 2001 From: Bianca Ragsdale <128001048+lai-bianca@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:38:43 -0700 Subject: [PATCH 2/4] docs: address Copilot review on README rewrite - api.md: point the v2 README link at the live #environments anchor - Make the Extract example standalone (define its parse call in-block) - Wrap the aiohttp example in an async function so it is runnable - strict=False: unsupported schema fields are skipped, not "reported" - Distinguish list() returning JobList from create/get/wait returning Job - Correct extract_jobs.create argument description (adds service_tier, no save_to) - Document that v2 helper exceptions inherit from LandingAiadeError, not APIError - Scope .with_raw_response / .with_streaming_response to v1 methods Kept the placeholder: angle brackets match the convention used across docs.landing.ai. Co-Authored-By: Claude Fable 5 --- README.md | 24 ++++++++++++++++-------- api.md | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3d74b26..ad701ab 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ If some pages cannot be parsed, the request still succeeds (HTTP 206) and `metad Use `client.v2.extract` to pull structured fields out of Markdown (typically from a parse response) using a schema. The `schema` parameter accepts a Pydantic `BaseModel` subclass, a `dict`, or a JSON string. Provide exactly one Markdown source: `markdown` or `markdown_url`. ```python +from pathlib import Path from pydantic import BaseModel, Field from landingai_ade import LandingAIADE @@ -118,6 +119,7 @@ class Person(BaseModel): age: int = Field(description="Person's age") client = LandingAIADE() +parsed = client.v2.parse(document=Path("path/to/file.pdf")) result = client.v2.extract( schema=Person, # Pydantic model, dict, or JSON string @@ -137,7 +139,7 @@ The response is a `V2ExtractResult`: | `extraction_metadata` | Mirrors `extraction`; each field carries the character spans in the Markdown that the value came from. | | `metadata` | Processing details, including credits used. | -By default, unsupported schema fields are pruned and reported. Pass `strict=True` to reject such schemas with an error (HTTP 422) instead. +By default, unsupported schema fields are skipped and extraction continues. Pass `strict=True` to reject such schemas with an error (HTTP 422) instead. ## Process Large Documents Asynchronously (Jobs) @@ -166,7 +168,7 @@ except JobFailedError as e: print(f"Job failed: {e}") ``` -Every job method returns a normalized `Job` with `job_id`, `status` (`pending`, `processing`, `completed`, `failed`, or `cancelled`), `progress`, `result`, `error`, and `raw` (the unmodified API envelope, for any field not surfaced on the typed model). +The `create`, `get`, and `wait` methods return a normalized `Job` with `job_id`, `status` (`pending`, `processing`, `completed`, `failed`, or `cancelled`), `progress`, `result`, `error`, and `raw` (the unmodified API envelope, for any field not surfaced on the typed model). The `list` method returns a `JobList`, a list of `Job` items that also carries pagination metadata (`has_more`, `page`, `page_size`). ```python # Poll manually instead of blocking @@ -179,7 +181,7 @@ for job in jobs: print(jobs.has_more) ``` -Extract jobs work the same way: `client.v2.extract_jobs` accepts the same arguments as `client.v2.extract`. +Extract jobs work the same way. The `create` method takes the same schema and Markdown arguments as `client.v2.extract`, plus `service_tier`; it does not accept `save_to`. ## Async Client @@ -205,10 +207,14 @@ pip install landingai-ade[aiohttp] ``` ```python +import asyncio from landingai_ade import AsyncLandingAIADE, DefaultAioHttpClient -async with AsyncLandingAIADE(http_client=DefaultAioHttpClient()) as client: - ... +async def main() -> None: + async with AsyncLandingAIADE(http_client=DefaultAioHttpClient()) as client: + ... # same usage as the example above + +asyncio.run(main()) ``` ## Environments @@ -260,11 +266,13 @@ for s in split.splits: ## Handling Errors -All errors inherit from `landingai_ade.APIError`. +All HTTP errors inherit from `landingai_ade.APIError`. - Connection problems raise a subclass of `landingai_ade.APIConnectionError`. - Non-success HTTP status codes (4xx, 5xx) raise a subclass of `landingai_ade.APIStatusError` with `status_code` and `response` properties. +The v2 helper exceptions are separate: `V2SyncTimeoutError`, `JobWaitTimeoutError`, and `JobFailedError` (importable from `landingai_ade.lib.v2_errors`) inherit from `LandingAiadeError` rather than `APIError`, so catch them explicitly as shown in the [jobs example](#process-large-documents-asynchronously-jobs). + ```python import landingai_ade from landingai_ade import LandingAIADE @@ -318,7 +326,7 @@ On timeout, an `APITimeoutError` is raised. Timed-out requests are retried twice ### Accessing raw response data (e.g. headers) -Prefix any method call with `.with_raw_response.` to get the raw HTTP response: +Prefix any v1 method call with `.with_raw_response.` to get the raw HTTP response (these wrappers cover the v1 methods only, not `client.v2`): ```python response = client.with_raw_response.parse(document=Path("file.pdf"), model="dpt-2-latest") @@ -326,7 +334,7 @@ print(response.headers.get("X-My-Header")) parsed = response.parse() # the object the method would have returned ``` -Use `.with_streaming_response` instead to stream the body rather than reading it eagerly; it requires a context manager and reads the body only when you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()`, or `.parse()`. These return [`APIResponse`](https://github.com/landing-ai/ade-python/tree/main/src/landingai_ade/_response.py) (or `AsyncAPIResponse`) objects. +Use `.with_streaming_response` instead (also v1 methods only) to stream the body rather than reading it eagerly; it requires a context manager and reads the body only when you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()`, or `.parse()`. These return [`APIResponse`](https://github.com/landing-ai/ade-python/tree/main/src/landingai_ade/_response.py) (or `AsyncAPIResponse`) objects. ### Nested params and file uploads diff --git a/api.md b/api.md index cba73d4..9d53ad9 100644 --- a/api.md +++ b/api.md @@ -62,7 +62,7 @@ Methods: # V2 -The `client.v2` sub-client targets LandingAI's next-generation ADE gateway, which lives on its own host (`api.ade.[env].landing.ai`) rather than the V1 host (`api.va.[env].landing.ai`). It is **additive**: `client.v2.*` is a separate surface from the top-level `client.*` (V1) methods documented above, and using it does not change any V1 behavior. See the [README](README.md#v2-api) for environment selection and usage examples. +The `client.v2` sub-client targets LandingAI's next-generation ADE gateway, which lives on its own host (`api.ade.[env].landing.ai`) rather than the V1 host (`api.va.[env].landing.ai`). It is **additive**: `client.v2.*` is a separate surface from the top-level `client.*` (V1) methods documented above, and using it does not change any V1 behavior. See the [README](README.md#environments) for environment selection and usage examples. `client.v2.parse_jobs` and `client.v2.extract_jobs` both return a single, unified `Job` shape, even though the underlying parse/extract job envelopes differ upstream -- `Job.raw` retains the full original envelope as an escape hatch for any field not surfaced on the typed model. From e96896533ed1a3d71724beb0c7dd7e3bf7b424ed Mon Sep 17 00:00:00 2001 From: Bianca Ragsdale <128001048+lai-bianca@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:56:19 -0700 Subject: [PATCH 3/4] docs: address second Copilot review round - Guard done.result in the jobs example: a cancelled job can be terminal with no result even with raise_on_failure=True - Add the missing markdown row to the V2ExtractResult field table - Distinguish client transport timeouts (APITimeoutError) from the v2 server wait window (HTTP 504 -> V2SyncTimeoutError) Co-Authored-By: Claude Fable 5 --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ad701ab..09243be 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ The response is a `V2ExtractResult`: | --- | --- | | `extraction` | The extracted values, matching your schema. | | `extraction_metadata` | Mirrors `extraction`; each field carries the character spans in the Markdown that the value came from. | +| `markdown` | The Markdown the extraction ran against, echoed back. | | `metadata` | Processing details, including credits used. | By default, unsupported schema fields are skipped and extraction continues. Pass `strict=True` to reject such schemas with an error (HTTP 422) instead. @@ -161,7 +162,8 @@ print(job.job_id, job.status) # Block until the job finishes (polls with backoff) try: done = client.v2.parse_jobs.wait(job.job_id, timeout=600, raise_on_failure=True) - print(done.result.markdown[:200]) + if done.result is not None: # a cancelled job can be terminal with no result + print(done.result.markdown[:200]) except JobWaitTimeoutError: print("Job did not finish in time; it is still running server-side.") except JobFailedError as e: @@ -320,7 +322,7 @@ client = LandingAIADE(timeout=20.0) # seconds client.with_options(timeout=5.0).v2.parse(...) # per-request ``` -On timeout, an `APITimeoutError` is raised. Timed-out requests are retried twice by default. +On a client-side transport timeout, an `APITimeoutError` is raised, and the request is retried twice by default. The v2 synchronous endpoints also have a server-side wait window: exceeding it returns HTTP 504 and raises `V2SyncTimeoutError` instead; switch to [jobs](#process-large-documents-asynchronously-jobs) for those documents. ## Advanced Usage From c5a34913c1420bb9ca06b8f1175d487bfeb92add Mon Sep 17 00:00:00 2001 From: Bianca Ragsdale <128001048+lai-bianca@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:08:34 -0700 Subject: [PATCH 4/4] docs: address third Copilot review round - JobList pagination: page/page_size are populated on extract job lists only; parse lists carry has_more (and org_id) - Error-handling example: catch V2SyncTimeoutError explicitly, since it bypasses the APIError hierarchy on v2 sync 504s Co-Authored-By: Claude Fable 5 --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 09243be..0f1bbbb 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ except JobFailedError as e: print(f"Job failed: {e}") ``` -The `create`, `get`, and `wait` methods return a normalized `Job` with `job_id`, `status` (`pending`, `processing`, `completed`, `failed`, or `cancelled`), `progress`, `result`, `error`, and `raw` (the unmodified API envelope, for any field not surfaced on the typed model). The `list` method returns a `JobList`, a list of `Job` items that also carries pagination metadata (`has_more`, `page`, `page_size`). +The `create`, `get`, and `wait` methods return a normalized `Job` with `job_id`, `status` (`pending`, `processing`, `completed`, `failed`, or `cancelled`), `progress`, `result`, `error`, and `raw` (the unmodified API envelope, for any field not surfaced on the typed model). The `list` method returns a `JobList`, a list of `Job` items that also carries pagination metadata: `has_more` on both endpoints, plus `page` and `page_size` on extract job lists only. ```python # Poll manually instead of blocking @@ -278,6 +278,7 @@ The v2 helper exceptions are separate: `V2SyncTimeoutError`, `JobWaitTimeoutErro ```python import landingai_ade from landingai_ade import LandingAIADE +from landingai_ade.lib.v2_errors import V2SyncTimeoutError client = LandingAIADE() @@ -286,6 +287,8 @@ try: except landingai_ade.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) +except V2SyncTimeoutError: + print("The synchronous request timed out; use parse_jobs for this document.") except landingai_ade.RateLimitError: print("A 429 status code was received; back off and retry.") except landingai_ade.APIStatusError as e: