Skip to content

Commit 89330f7

Browse files
stainless-app[bot]stainless-bot
authored andcommitted
feat(api): update via SDK Studio (#250)
1 parent c51b130 commit 89330f7

File tree

11 files changed

+98
-7
lines changed

11 files changed

+98
-7
lines changed

.devcontainer/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
33

44
USER vscode
55

6-
RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.24.0" RYE_INSTALL_OPTION="--yes" bash
6+
RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash
77
ENV PATH=/home/vscode/.rye/shims:$PATH
88

99
RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
curl -sSf https://rye.astral.sh/get | bash
2222
echo "$HOME/.rye/shims" >> $GITHUB_PATH
2323
env:
24-
RYE_VERSION: 0.24.0
24+
RYE_VERSION: '0.35.0'
2525
RYE_INSTALL_OPTION: '--yes'
2626

2727
- name: Install dependencies

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ jobs:
2121
curl -sSf https://rye.astral.sh/get | bash
2222
echo "$HOME/.rye/shims" >> $GITHUB_PATH
2323
env:
24-
RYE_VERSION: 0.24.0
25-
RYE_INSTALL_OPTION: "--yes"
24+
RYE_VERSION: '0.35.0'
25+
RYE_INSTALL_OPTION: '--yes'
2626

2727
- name: Publish to PyPI
2828
run: |

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.prism.log
12
.vscode
23
_dev
34

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ dev-dependencies = [
5858
"nox",
5959
"dirty-equals>=0.6.0",
6060
"importlib-metadata>=6.7.0",
61+
"rich>=13.7.1",
6162

6263
]
6364

requirements-dev.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# features: []
77
# all-features: true
88
# with-sources: false
9+
# generate-hashes: false
910

1011
-e file:.
1112
annotated-types==0.6.0
@@ -44,6 +45,10 @@ idna==3.4
4445
importlib-metadata==7.0.0
4546
iniconfig==2.0.0
4647
# via pytest
48+
markdown-it-py==3.0.0
49+
# via rich
50+
mdurl==0.1.2
51+
# via markdown-it-py
4752
mypy==1.7.1
4853
mypy-extensions==1.0.0
4954
# via mypy
@@ -67,6 +72,8 @@ pydantic==2.7.1
6772
# via openlayer
6873
pydantic-core==2.18.2
6974
# via pydantic
75+
pygments==2.18.0
76+
# via rich
7077
pyright==1.1.364
7178
pytest==7.1.1
7279
# via pytest-asyncio
@@ -78,6 +85,7 @@ pytz==2023.3.post1
7885
# via dirty-equals
7986
# via pandas
8087
respx==0.20.2
88+
rich==13.7.1
8189
ruff==0.1.9
8290
setuptools==68.2.2
8391
# via nodeenv

requirements.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# features: []
77
# all-features: true
88
# with-sources: false
9+
# generate-hashes: false
910

1011
-e file:.
1112
annotated-types==0.6.0

src/openlayer/_base_client.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
HttpxSendArgs,
5959
AsyncTransport,
6060
RequestOptions,
61+
HttpxRequestFiles,
6162
ModelBuilderProtocol,
6263
)
6364
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
@@ -459,6 +460,7 @@ def _build_request(
459460
headers = self._build_headers(options)
460461
params = _merge_mappings(self.default_query, options.params)
461462
content_type = headers.get("Content-Type")
463+
files = options.files
462464

463465
# If the given Content-Type header is multipart/form-data then it
464466
# has to be removed so that httpx can generate the header with
@@ -472,14 +474,23 @@ def _build_request(
472474
headers.pop("Content-Type")
473475

474476
# As we are now sending multipart/form-data instead of application/json
475-
# we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding
477+
# we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
476478
if json_data:
477479
if not is_dict(json_data):
478480
raise TypeError(
479481
f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead."
480482
)
481483
kwargs["data"] = self._serialize_multipartform(json_data)
482484

485+
# httpx determines whether or not to send a "multipart/form-data"
486+
# request based on the truthiness of the "files" argument.
487+
# This gets around that issue by generating a dict value that
488+
# evaluates to true.
489+
#
490+
# https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186
491+
if not files:
492+
files = cast(HttpxRequestFiles, ForceMultipartDict())
493+
483494
# TODO: report this error to httpx
484495
return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
485496
headers=headers,
@@ -492,7 +503,7 @@ def _build_request(
492503
# https://github.com/microsoft/pyright/issues/3526#event-6715453066
493504
params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
494505
json=json_data,
495-
files=options.files,
506+
files=files,
496507
**kwargs,
497508
)
498509

@@ -1863,6 +1874,11 @@ def make_request_options(
18631874
return options
18641875

18651876

1877+
class ForceMultipartDict(Dict[str, None]):
1878+
def __bool__(self) -> bool:
1879+
return True
1880+
1881+
18661882
class OtherPlatform:
18671883
def __init__(self, name: str) -> None:
18681884
self.name = name

src/openlayer/_models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
ClassVar,
1111
Protocol,
1212
Required,
13+
ParamSpec,
1314
TypedDict,
1415
TypeGuard,
1516
final,
@@ -67,6 +68,9 @@
6768
__all__ = ["BaseModel", "GenericModel"]
6869

6970
_T = TypeVar("_T")
71+
_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel")
72+
73+
P = ParamSpec("P")
7074

7175

7276
@runtime_checkable
@@ -379,6 +383,29 @@ def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericMo
379383
return issubclass(origin, BaseModel) or issubclass(origin, GenericModel)
380384

381385

386+
def build(
387+
base_model_cls: Callable[P, _BaseModelT],
388+
*args: P.args,
389+
**kwargs: P.kwargs,
390+
) -> _BaseModelT:
391+
"""Construct a BaseModel class without validation.
392+
393+
This is useful for cases where you need to instantiate a `BaseModel`
394+
from an API response as this provides type-safe params which isn't supported
395+
by helpers like `construct_type()`.
396+
397+
```py
398+
build(MyModel, my_field_a="foo", my_field_b=123)
399+
```
400+
"""
401+
if args:
402+
raise TypeError(
403+
"Received positional arguments which are not supported; Keyword arguments must be used instead",
404+
)
405+
406+
return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs))
407+
408+
382409
def construct_type(*, value: object, type_: object) -> object:
383410
"""Loose coercion to the expected type with construction of nested values.
384411

src/openlayer/_utils/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,7 @@
4949
maybe_transform as maybe_transform,
5050
async_maybe_transform as async_maybe_transform,
5151
)
52-
from ._reflection import function_has_argument as function_has_argument
52+
from ._reflection import (
53+
function_has_argument as function_has_argument,
54+
assert_signatures_in_sync as assert_signatures_in_sync,
55+
)

src/openlayer/_utils/_reflection.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import inspect
24
from typing import Any, Callable
35

@@ -6,3 +8,35 @@ def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool:
68
"""Returns whether or not the given function has a specific parameter"""
79
sig = inspect.signature(func)
810
return arg_name in sig.parameters
11+
12+
13+
def assert_signatures_in_sync(
14+
source_func: Callable[..., Any],
15+
check_func: Callable[..., Any],
16+
*,
17+
exclude_params: set[str] = set(),
18+
) -> None:
19+
"""Ensure that the signature of the second function matches the first."""
20+
21+
check_sig = inspect.signature(check_func)
22+
source_sig = inspect.signature(source_func)
23+
24+
errors: list[str] = []
25+
26+
for name, source_param in source_sig.parameters.items():
27+
if name in exclude_params:
28+
continue
29+
30+
custom_param = check_sig.parameters.get(name)
31+
if not custom_param:
32+
errors.append(f"the `{name}` param is missing")
33+
continue
34+
35+
if custom_param.annotation != source_param.annotation:
36+
errors.append(
37+
f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(source_param.annotation)}"
38+
)
39+
continue
40+
41+
if errors:
42+
raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors))

0 commit comments

Comments
 (0)