Skip to content

Commit b13032b

Browse files
release: 1.11.1 (#87)
* codegen metadata * perf(client): optimize file structure copying in multipart requests * codegen metadata * release: 1.11.1 --------- Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
1 parent 7d7f94b commit b13032b

12 files changed

Lines changed: 198 additions & 106 deletions

File tree

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "1.11.0"
2+
".": "1.11.1"
33
}

.stats.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 7
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/landingai%2Fade-7247e8d6fb71965ebfc666fb0c1379394b2d7744be2bb304e30e04acfa8855f8.yml
3-
openapi_spec_hash: 5511c4c03ee213ceaf68c9f9d889f861
4-
config_hash: 2b7c8ce95f04ab9eea27533da72f6234
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/landingai%2Fade-3052ba97a9203f16ff5e3f37520554ebdb5b05fd30a472c95869ed679ed30d1b.yml
3+
openapi_spec_hash: 3a45b51e72f53055cc87a88d1f9ad05e
4+
config_hash: 56c9f6570b7996559a642d8de67b400f

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 1.11.1 (2026-04-22)
4+
5+
Full Changelog: [v1.11.0...v1.11.1](https://github.com/landing-ai/ade-python/compare/v1.11.0...v1.11.1)
6+
7+
### Performance Improvements
8+
9+
* **client:** optimize file structure copying in multipart requests ([e78f8c0](https://github.com/landing-ai/ade-python/commit/e78f8c0604a7d59671998f02b7bb1832d8c09aa3))
10+
311
## 1.11.0 (2026-04-13)
412

513
Full Changelog: [v1.10.0...v1.11.0](https://github.com/landing-ai/ade-python/compare/v1.10.0...v1.11.0)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "landingai-ade"
3-
version = "1.11.0"
3+
version = "1.11.1"
44
description = "The official Python library for the landingai-ade API"
55
dynamic = ["readme"]
66
license = "Apache-2.0"

src/landingai_ade/_client.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from . import _exceptions
1515
from ._qs import Querystring
1616
from .types import client_parse_params, client_split_params, client_extract_params, client_extract_build_schema_params
17+
from ._files import deepcopy_with_paths
1718
from ._types import (
1819
Body,
1920
Omit,
@@ -33,7 +34,6 @@
3334
is_given,
3435
extract_files,
3536
maybe_transform,
36-
deepcopy_minimal,
3737
get_async_library,
3838
async_maybe_transform,
3939
)
@@ -348,14 +348,15 @@ def extract(
348348
# Convert local file paths to file parameters
349349
markdown, markdown_url = convert_url_to_file_if_local(markdown, markdown_url)
350350

351-
body = deepcopy_minimal(
351+
body = deepcopy_with_paths(
352352
{
353353
"schema": schema,
354354
"markdown": markdown,
355355
"markdown_url": markdown_url,
356356
"model": model,
357357
"strict": strict,
358-
}
358+
},
359+
[["markdown"]],
359360
)
360361
files = extract_files(cast(Mapping[str, object], body), paths=[["markdown"]])
361362
# It should be noted that the actual Content-Type header that will be
@@ -430,14 +431,15 @@ def extract_build_schema(
430431
431432
timeout: Override the client-level default timeout for this request, in seconds
432433
"""
433-
body = deepcopy_minimal(
434+
body = deepcopy_with_paths(
434435
{
435436
"markdown_urls": markdown_urls,
436437
"markdowns": markdowns,
437438
"model": model,
438439
"prompt": prompt,
439440
"schema": schema,
440-
}
441+
},
442+
[["markdowns", "<array>"]],
441443
)
442444
files = extract_files(cast(Mapping[str, object], body), paths=[["markdowns", "<array>"]])
443445
# It should be noted that the actual Content-Type header that will be
@@ -520,15 +522,16 @@ def parse(
520522
# Convert local file paths to file parameters
521523
document, document_url = convert_url_to_file_if_local(document, document_url)
522524

523-
body = deepcopy_minimal(
525+
body = deepcopy_with_paths(
524526
{
525527
"custom_prompts": custom_prompts,
526528
"document": document,
527529
"document_url": document_url,
528530
"model": model,
529531
"password": password,
530532
"split": split,
531-
}
533+
},
534+
[["document"]],
532535
)
533536
files = extract_files(cast(Mapping[str, object], body), paths=[["document"]])
534537
# It should be noted that the actual Content-Type header that will be
@@ -606,13 +609,14 @@ def split(
606609
# Store original inputs for filename extraction
607610
original_markdown = markdown
608611
original_markdown_url = markdown_url
609-
body = deepcopy_minimal(
612+
body = deepcopy_with_paths(
610613
{
611614
"split_class": split_class,
612615
"markdown": markdown,
613616
"markdown_url": markdown_url,
614617
"model": model,
615-
}
618+
},
619+
[["markdown"]],
616620
)
617621
files = extract_files(cast(Mapping[str, object], body), paths=[["markdown"]])
618622
# It should be noted that the actual Content-Type header that will be
@@ -885,14 +889,15 @@ async def extract(
885889
# Convert local file paths to file parameters
886890
markdown, markdown_url = convert_url_to_file_if_local(markdown, markdown_url)
887891

888-
body = deepcopy_minimal(
892+
body = deepcopy_with_paths(
889893
{
890894
"schema": schema,
891895
"markdown": markdown,
892896
"markdown_url": markdown_url,
893897
"model": model,
894898
"strict": strict,
895-
}
899+
},
900+
[["markdown"]],
896901
)
897902
files = extract_files(cast(Mapping[str, object], body), paths=[["markdown"]])
898903
# It should be noted that the actual Content-Type header that will be
@@ -963,14 +968,15 @@ async def extract_build_schema(
963968
964969
timeout: Override the client-level default timeout for this request, in seconds
965970
"""
966-
body = deepcopy_minimal(
971+
body = deepcopy_with_paths(
967972
{
968973
"markdown_urls": markdown_urls,
969974
"markdowns": markdowns,
970975
"model": model,
971976
"prompt": prompt,
972977
"schema": schema,
973-
}
978+
},
979+
[["markdowns", "<array>"]],
974980
)
975981
files = extract_files(cast(Mapping[str, object], body), paths=[["markdowns", "<array>"]])
976982
# It should be noted that the actual Content-Type header that will be
@@ -1045,15 +1051,16 @@ async def parse(
10451051
# Convert local file paths to file parameters
10461052
document, document_url = convert_url_to_file_if_local(document, document_url)
10471053

1048-
body = deepcopy_minimal(
1054+
body = deepcopy_with_paths(
10491055
{
10501056
"custom_prompts": custom_prompts,
10511057
"document": document,
10521058
"document_url": document_url,
10531059
"model": model,
10541060
"password": password,
10551061
"split": split,
1056-
}
1062+
},
1063+
[["document"]],
10571064
)
10581065
files = extract_files(cast(Mapping[str, object], body), paths=[["document"]])
10591066
# It should be noted that the actual Content-Type header that will be
@@ -1119,13 +1126,14 @@ async def split(
11191126
11201127
timeout: Override the client-level default timeout for this request, in seconds
11211128
"""
1122-
body = deepcopy_minimal(
1129+
body = deepcopy_with_paths(
11231130
{
11241131
"split_class": split_class,
11251132
"markdown": markdown,
11261133
"markdown_url": markdown_url,
11271134
"model": model,
1128-
}
1135+
},
1136+
[["markdown"]],
11291137
)
11301138
files = extract_files(cast(Mapping[str, object], body), paths=[["markdown"]])
11311139
# It should be noted that the actual Content-Type header that will be

src/landingai_ade/_files.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import io
44
import os
55
import pathlib
6-
from typing import overload
7-
from typing_extensions import TypeGuard
6+
from typing import Sequence, cast, overload
7+
from typing_extensions import TypeVar, TypeGuard
88

99
import anyio
1010

@@ -17,7 +17,9 @@
1717
HttpxFileContent,
1818
HttpxRequestFiles,
1919
)
20-
from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
20+
from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t
21+
22+
_T = TypeVar("_T")
2123

2224

2325
def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
@@ -132,3 +134,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
132134
return file.encode('utf-8')
133135

134136
return file
137+
138+
139+
def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T:
140+
"""Copy only the containers along the given paths.
141+
142+
Used to guard against mutation by extract_files without copying the entire structure.
143+
Only dicts and lists that lie on a path are copied; everything else
144+
is returned by reference.
145+
146+
For example, given paths=[["foo", "files", "file"]] and the structure:
147+
{
148+
"foo": {
149+
"bar": {"baz": {}},
150+
"files": {"file": <content>}
151+
}
152+
}
153+
The root dict, "foo", and "files" are copied (they lie on the path).
154+
"bar" and "baz" are returned by reference (off the path).
155+
"""
156+
return _deepcopy_with_paths(item, paths, 0)
157+
158+
159+
def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T:
160+
if not paths:
161+
return item
162+
if is_mapping(item):
163+
key_to_paths: dict[str, list[Sequence[str]]] = {}
164+
for path in paths:
165+
if index < len(path):
166+
key_to_paths.setdefault(path[index], []).append(path)
167+
168+
# if no path continues through this mapping, it won't be mutated and copying it is redundant
169+
if not key_to_paths:
170+
return item
171+
172+
result = dict(item)
173+
for key, subpaths in key_to_paths.items():
174+
if key in result:
175+
result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1)
176+
return cast(_T, result)
177+
if is_list(item):
178+
array_paths = [path for path in paths if index < len(path) and path[index] == "<array>"]
179+
180+
# if no path expects a list here, nothing will be mutated inside it - return by reference
181+
if not array_paths:
182+
return cast(_T, item)
183+
return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item])
184+
return item

src/landingai_ade/_utils/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
coerce_integer as coerce_integer,
2525
file_from_path as file_from_path,
2626
strip_not_given as strip_not_given,
27-
deepcopy_minimal as deepcopy_minimal,
2827
get_async_library as get_async_library,
2928
maybe_coerce_float as maybe_coerce_float,
3029
get_required_header as get_required_header,

src/landingai_ade/_utils/_utils.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
177177
return isinstance(obj, Iterable)
178178

179179

180-
def deepcopy_minimal(item: _T) -> _T:
181-
"""Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
182-
183-
- mappings, e.g. `dict`
184-
- list
185-
186-
This is done for performance reasons.
187-
"""
188-
if is_mapping(item):
189-
return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
190-
if is_list(item):
191-
return cast(_T, [deepcopy_minimal(entry) for entry in item])
192-
return item
193-
194-
195180
# copied from https://github.com/Rapptz/RoboDanny
196181
def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
197182
size = len(seq)

src/landingai_ade/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "landingai_ade"
4-
__version__ = "1.11.0" # x-release-please-version
4+
__version__ = "1.11.1" # x-release-please-version

src/landingai_ade/resources/parse_jobs.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import httpx
99

1010
from ..types import parse_job_list_params, parse_job_create_params
11+
from .._files import deepcopy_with_paths
1112
from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given
12-
from .._utils import extract_files, path_template, maybe_transform, deepcopy_minimal, async_maybe_transform
13+
from .._utils import extract_files, path_template, maybe_transform, async_maybe_transform
1314
from .._compat import cached_property
1415
from .._resource import SyncAPIResource, AsyncAPIResource
1516
from .._response import (
@@ -106,7 +107,7 @@ def create(
106107
107108
timeout: Override the client-level default timeout for this request, in seconds
108109
"""
109-
body = deepcopy_minimal(
110+
body = deepcopy_with_paths(
110111
{
111112
"custom_prompts": custom_prompts,
112113
"document": document,
@@ -115,7 +116,8 @@ def create(
115116
"output_save_url": output_save_url,
116117
"password": password,
117118
"split": split,
118-
}
119+
},
120+
[["document"]],
119121
)
120122
files = extract_files(cast(Mapping[str, object], body), paths=[["document"]])
121123
# It should be noted that the actual Content-Type header that will be
@@ -304,7 +306,7 @@ async def create(
304306
305307
timeout: Override the client-level default timeout for this request, in seconds
306308
"""
307-
body = deepcopy_minimal(
309+
body = deepcopy_with_paths(
308310
{
309311
"custom_prompts": custom_prompts,
310312
"document": document,
@@ -313,7 +315,8 @@ async def create(
313315
"output_save_url": output_save_url,
314316
"password": password,
315317
"split": split,
316-
}
318+
},
319+
[["document"]],
317320
)
318321
files = extract_files(cast(Mapping[str, object], body), paths=[["document"]])
319322
# It should be noted that the actual Content-Type header that will be

0 commit comments

Comments
 (0)