Skip to content

Commit e31d874

Browse files
Merge pull request #19 from CASParser/release-please--branches--main--changes--next
release: 1.8.0
2 parents b169642 + 4da987b commit e31d874

24 files changed

Lines changed: 352 additions & 214 deletions

.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.7.0"
2+
".": "1.8.0"
33
}

.stats.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 21
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-d9763d006969b49a1473851069fdfa429eb13133b64103a62963bb70ddb22305.yml
3-
openapi_spec_hash: 6aee689b7a759b12c85c088c15e29bc0
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-e5c0c65637cdf3a6c4360b8193973b73a3d35ad1056ef607c3319ef03e591a55.yml
3+
openapi_spec_hash: 7515d1e5fe3130b9f5411f7aacbc8a64
44
config_hash: 5509bb7a961ae2e79114b24c381606d4

CHANGELOG.md

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

3+
## 1.8.0 (2026-04-19)
4+
5+
Full Changelog: [v1.7.0...v1.8.0](https://github.com/CASParser/cas-parser-python/compare/v1.7.0...v1.8.0)
6+
7+
### Features
8+
9+
* **api:** api update ([78f7f97](https://github.com/CASParser/cas-parser-python/commit/78f7f97e1f17e44bc4443c43e92baf1888b2808a))
10+
* **api:** api update ([2e57117](https://github.com/CASParser/cas-parser-python/commit/2e571178acf86c879c727a43cff97ab4e05c1e2d))
11+
12+
13+
### Bug Fixes
14+
15+
* **client:** preserve hardcoded query params when merging with user params ([3e5eea1](https://github.com/CASParser/cas-parser-python/commit/3e5eea1e92928bd98243022c607f6f15afa24d52))
16+
* ensure file data are only sent as 1 parameter ([4985a34](https://github.com/CASParser/cas-parser-python/commit/4985a349eee6f59007dfdf770c26deba75acc7dd))
17+
18+
19+
### Performance Improvements
20+
21+
* **client:** optimize file structure copying in multipart requests ([1c854af](https://github.com/CASParser/cas-parser-python/commit/1c854af1baa2f1e702881692b65e9a85efffedd5))
22+
323
## 1.7.0 (2026-03-27)
424

525
Full Changelog: [v1.6.3...v1.7.0](https://github.com/CASParser/cas-parser-python/compare/v1.6.3...v1.7.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 = "cas-parser-python"
3-
version = "1.7.0"
3+
version = "1.8.0"
44
description = "The official Python library for the cas-parser API"
55
dynamic = ["readme"]
66
license = "Apache-2.0"

src/cas_parser/_base_client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,10 @@ def _build_request(
558558
files = cast(HttpxRequestFiles, ForceMultipartDict())
559559

560560
prepared_url = self._prepare_url(options.url)
561+
# preserve hard-coded query params from the url
562+
if params and prepared_url.query:
563+
params = {**dict(prepared_url.params.items()), **params}
564+
prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0])
561565
if "_" in prepared_url.host:
562566
# work around https://github.com/encode/httpx/discussions/2880
563567
kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}

src/cas_parser/_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]:
@@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
121123
return await anyio.Path(file).read_bytes()
122124

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

src/cas_parser/_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/cas_parser/_utils/_utils.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ def _extract_items(
8686
index += 1
8787
if is_dict(obj):
8888
try:
89-
# We are at the last entry in the path so we must remove the field
90-
if (len(path)) == index:
89+
# Remove the field if there are no more dict keys in the path,
90+
# only "<array>" traversal markers or end.
91+
if all(p == "<array>" for p in path[index:]):
9192
item = obj.pop(key)
9293
else:
9394
item = obj[key]
@@ -176,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
176177
return isinstance(obj, Iterable)
177178

178179

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

src/cas_parser/_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__ = "cas_parser"
4-
__version__ = "1.7.0" # x-release-please-version
4+
__version__ = "1.8.0" # x-release-please-version

src/cas_parser/resources/cams_kfintech.py

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

99
from ..types import cams_kfintech_parse_params
10+
from .._files import deepcopy_with_paths
1011
from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
11-
from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform
12+
from .._utils import extract_files, maybe_transform, async_maybe_transform
1213
from .._compat import cached_property
1314
from .._resource import SyncAPIResource, AsyncAPIResource
1415
from .._response import (
@@ -78,12 +79,13 @@ def parse(
7879
7980
timeout: Override the client-level default timeout for this request, in seconds
8081
"""
81-
body = deepcopy_minimal(
82+
body = deepcopy_with_paths(
8283
{
8384
"password": password,
8485
"pdf_file": pdf_file,
8586
"pdf_url": pdf_url,
86-
}
87+
},
88+
[["pdf_file"]],
8789
)
8890
files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]])
8991
if files:
@@ -157,12 +159,13 @@ async def parse(
157159
158160
timeout: Override the client-level default timeout for this request, in seconds
159161
"""
160-
body = deepcopy_minimal(
162+
body = deepcopy_with_paths(
161163
{
162164
"password": password,
163165
"pdf_file": pdf_file,
164166
"pdf_url": pdf_url,
165-
}
167+
},
168+
[["pdf_file"]],
166169
)
167170
files = extract_files(cast(Mapping[str, object], body), paths=[["pdf_file"]])
168171
if files:

0 commit comments

Comments
 (0)