Skip to content

Commit c265056

Browse files
feat(api): api update
1 parent 93a9613 commit c265056

File tree

7 files changed

+174
-10
lines changed

7 files changed

+174
-10
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
- name: Get GitHub OIDC Token
6464
if: github.repository == 'stainless-sdks/cas-parser-python'
6565
id: github-oidc
66-
uses: actions/github-script@v6
66+
uses: actions/github-script@v8
6767
with:
6868
script: core.setOutput('github_token', await core.getIDToken());
6969

.stats.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 4
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-ce2296c4b14d27c141bb2745607d2456c923fdca3ae0a0a0800c26e564333850.yml
3-
openapi_spec_hash: 8eb586ccf16b534c0c15ff6a22274c7d
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cas-parser%2Fcas-parser-2e3df9c77e887f49ca3dffd5d68f30a8a0ea0b557f31282dd191ce85713e3e34.yml
3+
openapi_spec_hash: 1cb90023118602a40a106cd51ed6a926
44
config_hash: cb5d75abef6264b5d86448caf7295afa

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/).
1313

1414
Use the Cas Parser 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.
1515

16-
[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbm9kZS1tY3AiXX0)
17-
[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-node-mcp%22%5D%7D)
16+
[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=cas-parser-node-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImNhcy1wYXJzZXItbm9kZS1tY3AiXSwiZW52Ijp7IkNBU19QQVJTRVJfQVBJX0tFWSI6Ik15IEFQSSBLZXkifX0)
17+
[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22cas-parser-node-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22cas-parser-node-mcp%22%5D%2C%22env%22%3A%7B%22CAS_PARSER_API_KEY%22%3A%22My%20API%20Key%22%7D%7D)
1818

1919
> Note: You may need to set environment variables in your MCP client.
2020

src/cas_parser/_base_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
APIConnectionError,
8787
APIResponseValidationError,
8888
)
89+
from ._utils._json import openapi_dumps
8990

9091
log: logging.Logger = logging.getLogger(__name__)
9192

@@ -554,8 +555,10 @@ def _build_request(
554555
kwargs["content"] = options.content
555556
elif isinstance(json_data, bytes):
556557
kwargs["content"] = json_data
557-
else:
558-
kwargs["json"] = json_data if is_given(json_data) else None
558+
elif not files:
559+
# Don't set content when JSON is sent as multipart/form-data,
560+
# since httpx's content param overrides other body arguments
561+
kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None
559562
kwargs["files"] = files
560563
else:
561564
headers.pop("Content-Type", None)

src/cas_parser/_compat.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def model_dump(
139139
exclude_defaults: bool = False,
140140
warnings: bool = True,
141141
mode: Literal["json", "python"] = "python",
142+
by_alias: bool | None = None,
142143
) -> dict[str, Any]:
143144
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
144145
return model.model_dump(
@@ -148,13 +149,12 @@ def model_dump(
148149
exclude_defaults=exclude_defaults,
149150
# warnings are not supported in Pydantic v1
150151
warnings=True if PYDANTIC_V1 else warnings,
152+
by_alias=by_alias,
151153
)
152154
return cast(
153155
"dict[str, Any]",
154156
model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast]
155-
exclude=exclude,
156-
exclude_unset=exclude_unset,
157-
exclude_defaults=exclude_defaults,
157+
exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias)
158158
),
159159
)
160160

src/cas_parser/_utils/_json.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import json
2+
from typing import Any
3+
from datetime import datetime
4+
from typing_extensions import override
5+
6+
import pydantic
7+
8+
from .._compat import model_dump
9+
10+
11+
def openapi_dumps(obj: Any) -> bytes:
12+
"""
13+
Serialize an object to UTF-8 encoded JSON bytes.
14+
15+
Extends the standard json.dumps with support for additional types
16+
commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc.
17+
"""
18+
return json.dumps(
19+
obj,
20+
cls=_CustomEncoder,
21+
# Uses the same defaults as httpx's JSON serialization
22+
ensure_ascii=False,
23+
separators=(",", ":"),
24+
allow_nan=False,
25+
).encode()
26+
27+
28+
class _CustomEncoder(json.JSONEncoder):
29+
@override
30+
def default(self, o: Any) -> Any:
31+
if isinstance(o, datetime):
32+
return o.isoformat()
33+
if isinstance(o, pydantic.BaseModel):
34+
return model_dump(o, exclude_unset=True, mode="json", by_alias=True)
35+
return super().default(o)

tests/test_utils/test_json.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from __future__ import annotations
2+
3+
import datetime
4+
from typing import Union
5+
6+
import pydantic
7+
8+
from cas_parser import _compat
9+
from cas_parser._utils._json import openapi_dumps
10+
11+
12+
class TestOpenapiDumps:
13+
def test_basic(self) -> None:
14+
data = {"key": "value", "number": 42}
15+
json_bytes = openapi_dumps(data)
16+
assert json_bytes == b'{"key":"value","number":42}'
17+
18+
def test_datetime_serialization(self) -> None:
19+
dt = datetime.datetime(2023, 1, 1, 12, 0, 0)
20+
data = {"datetime": dt}
21+
json_bytes = openapi_dumps(data)
22+
assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}'
23+
24+
def test_pydantic_model_serialization(self) -> None:
25+
class User(pydantic.BaseModel):
26+
first_name: str
27+
last_name: str
28+
age: int
29+
30+
model_instance = User(first_name="John", last_name="Kramer", age=83)
31+
data = {"model": model_instance}
32+
json_bytes = openapi_dumps(data)
33+
assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}'
34+
35+
def test_pydantic_model_with_default_values(self) -> None:
36+
class User(pydantic.BaseModel):
37+
name: str
38+
role: str = "user"
39+
active: bool = True
40+
score: int = 0
41+
42+
model_instance = User(name="Alice")
43+
data = {"model": model_instance}
44+
json_bytes = openapi_dumps(data)
45+
assert json_bytes == b'{"model":{"name":"Alice"}}'
46+
47+
def test_pydantic_model_with_default_values_overridden(self) -> None:
48+
class User(pydantic.BaseModel):
49+
name: str
50+
role: str = "user"
51+
active: bool = True
52+
53+
model_instance = User(name="Bob", role="admin", active=False)
54+
data = {"model": model_instance}
55+
json_bytes = openapi_dumps(data)
56+
assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}'
57+
58+
def test_pydantic_model_with_alias(self) -> None:
59+
class User(pydantic.BaseModel):
60+
first_name: str = pydantic.Field(alias="firstName")
61+
last_name: str = pydantic.Field(alias="lastName")
62+
63+
model_instance = User(firstName="John", lastName="Doe")
64+
data = {"model": model_instance}
65+
json_bytes = openapi_dumps(data)
66+
assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}'
67+
68+
def test_pydantic_model_with_alias_and_default(self) -> None:
69+
class User(pydantic.BaseModel):
70+
user_name: str = pydantic.Field(alias="userName")
71+
user_role: str = pydantic.Field(default="member", alias="userRole")
72+
is_active: bool = pydantic.Field(default=True, alias="isActive")
73+
74+
model_instance = User(userName="charlie")
75+
data = {"model": model_instance}
76+
json_bytes = openapi_dumps(data)
77+
assert json_bytes == b'{"model":{"userName":"charlie"}}'
78+
79+
model_with_overrides = User(userName="diana", userRole="admin", isActive=False)
80+
data = {"model": model_with_overrides}
81+
json_bytes = openapi_dumps(data)
82+
assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}'
83+
84+
def test_pydantic_model_with_nested_models_and_defaults(self) -> None:
85+
class Address(pydantic.BaseModel):
86+
street: str
87+
city: str = "Unknown"
88+
89+
class User(pydantic.BaseModel):
90+
name: str
91+
address: Address
92+
verified: bool = False
93+
94+
if _compat.PYDANTIC_V1:
95+
# to handle forward references in Pydantic v1
96+
User.update_forward_refs(**locals()) # type: ignore[reportDeprecated]
97+
98+
address = Address(street="123 Main St")
99+
user = User(name="Diana", address=address)
100+
data = {"user": user}
101+
json_bytes = openapi_dumps(data)
102+
assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}'
103+
104+
address_with_city = Address(street="456 Oak Ave", city="Boston")
105+
user_verified = User(name="Eve", address=address_with_city, verified=True)
106+
data = {"user": user_verified}
107+
json_bytes = openapi_dumps(data)
108+
assert (
109+
json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}'
110+
)
111+
112+
def test_pydantic_model_with_optional_fields(self) -> None:
113+
class User(pydantic.BaseModel):
114+
name: str
115+
email: Union[str, None]
116+
phone: Union[str, None]
117+
118+
model_with_none = User(name="Eve", email=None, phone=None)
119+
data = {"model": model_with_none}
120+
json_bytes = openapi_dumps(data)
121+
assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}'
122+
123+
model_with_values = User(name="Frank", email="frank@example.com", phone=None)
124+
data = {"model": model_with_values}
125+
json_bytes = openapi_dumps(data)
126+
assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}'

0 commit comments

Comments
 (0)