From 4e48bab02903767f82cb1448f5bc334edb22185c Mon Sep 17 00:00:00 2001 From: Zhichao Date: Mon, 13 Jul 2026 20:10:07 +0800 Subject: [PATCH] fix(v2): drop stale extract params & fix README drift from aide spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tech-writer review against aide surfaced SDK parameters that were dropped from the API: - Remove `markdown_ref` from `client.v2.extract` / `extract_jobs.create` (dropped in aide#951); extract now requires exactly one of `markdown` or `markdown_url`. Delete the README section documenting it. - Remove `idempotency_key` from the same methods (dropped in aide#944). - Fix the README jobs example to use `service_tier="priority"` — the code was already renamed from `priority` (aide#899) in #106, but the example still passed a nonexistent kwarg that raised TypeError. - Reword `files.upload` docstrings/api.md to stop referencing the removed `markdown_ref` (`/v1/files` itself is still in the spec). `password` on v2 parse is intentionally kept: it remains in the tracked spec (specs/v2-aide.json) as a placeholder until PDF decryption ships (aide#272, aide#390). No released-surface break: client.v2 shipped after v1.12.0 (griffe surface-lock clean). Full suite: 587 passed; ruff/mypy clean. Co-Authored-By: Claude Fable 5 --- README.md | 16 +--------- api.md | 8 ++--- src/landingai_ade/resources/v2/extract.py | 37 +++-------------------- src/landingai_ade/resources/v2/files.py | 4 +-- src/landingai_ade/resources/v2/v2.py | 8 ----- tests/api_resources/v2/test_extract.py | 5 ++- 6 files changed, 14 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 70a842c..1edc5ce 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. diff --git a/api.md b/api.md index 633045a..cba73d4 100644 --- a/api.md +++ b/api.md @@ -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). -- client.v2.extract(\*, schema, markdown=..., markdown_ref=..., markdown_url=..., model=..., strict=..., idempotency_key=..., save_to=...) -> V2ExtractResult +- client.v2.extract(\*, schema, markdown=..., markdown_url=..., model=..., strict=..., save_to=...) -> V2ExtractResult - 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. -- client.v2.extract_jobs.create(\*, schema, markdown=..., markdown_ref=..., markdown_url=..., model=..., strict=..., idempotency_key=..., service_tier=...) -> Job +- client.v2.extract_jobs.create(\*, schema, markdown=..., markdown_url=..., model=..., strict=..., service_tier=...) -> Job - client.v2.extract_jobs.get(job_id) -> Job - client.v2.extract_jobs.list(\*, page=..., page_size=..., status=...) -> JobList[Job] - client.v2.extract_jobs.wait(job_id, \*, timeout=600, poll_interval=None, raise_on_failure=False) -> Job @@ -121,7 +121,7 @@ Methods: - client.v2.files.upload(\*, file) -> str - 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: diff --git a/src/landingai_ade/resources/v2/extract.py b/src/landingai_ade/resources/v2/extract.py index 266bb1c..0261eec 100644 --- a/src/landingai_ade/resources/v2/extract.py +++ b/src/landingai_ade/resources/v2/extract.py @@ -25,18 +25,15 @@ 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 @@ -44,17 +41,15 @@ def _build_extract_body( 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`" + (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: @@ -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. @@ -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. @@ -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 @@ -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"), @@ -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. @@ -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"), @@ -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. @@ -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. @@ -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 @@ -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, @@ -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. @@ -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, diff --git a/src/landingai_ade/resources/v2/files.py b/src/landingai_ade/resources/v2/files.py index fd25d97..a88b566 100644 --- a/src/landingai_ade/resources/v2/files.py +++ b/src/landingai_ade/resources/v2/files.py @@ -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 {})} @@ -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 {})} diff --git a/src/landingai_ade/resources/v2/v2.py b/src/landingai_ade/resources/v2/v2.py index ee20a69..befa049 100644 --- a/src/landingai_ade/resources/v2/v2.py +++ b/src/landingai_ade/resources/v2/v2.py @@ -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. @@ -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, @@ -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. @@ -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, diff --git a/tests/api_resources/v2/test_extract.py b/tests/api_resources/v2/test_extract.py index 4af2da2..becaf09 100644 --- a/tests/api_resources/v2/test_extract.py +++ b/tests/api_resources/v2/test_extract.py @@ -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") @@ -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.