diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4208b5cb..1b77f506 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.7.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 535cb6ea..69b72815 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 25 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-efa19cc3cc7e8d1692f1952185eb882fd6250d5bc81af147aab3830fc39b4f8d.yml -openapi_spec_hash: 85a6c998ec4fbd8d526ccd3dd40bdf96 -config_hash: 42515bf83f1e0e765071038fcf702122 +configured_endpoints: 27 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-c74e7376ab152155b24a3f8f21fa43709cbc94b6e8b33a6ed48a23a62f976d1a.yml +openapi_spec_hash: f430c7e1c4f367245df8145df75fb443 +config_hash: e88d2c04584e96ef1e76c72156c3c186 diff --git a/CHANGELOG.md b/CHANGELOG.md index d7913bf9..2d78c9b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.7.0 (2025-07-12) + +Full Changelog: [v0.6.0...v0.7.0](https://github.com/steel-dev/steel-python/compare/v0.6.0...v0.7.0) + +### Features + +* **api:** api update ([7c3a807](https://github.com/steel-dev/steel-python/commit/7c3a8071ad39d7d371c68bc5632630aba52d1126)) + + +### Bug Fixes + +* **client:** don't send Content-Type header on GET requests ([3663409](https://github.com/steel-dev/steel-python/commit/366340962d2ad7252fc25b70399293cb2b8687aa)) +* **parsing:** correctly handle nested discriminated unions ([d88c056](https://github.com/steel-dev/steel-python/commit/d88c05684f94be0249181c40365ae1605e65c9d8)) + + +### Chores + +* **internal:** bump pinned h11 dep ([f79c166](https://github.com/steel-dev/steel-python/commit/f79c16621c3a9e5b902f28294a82638f7165cf49)) +* **package:** mark python 3.13 as supported ([a70bc61](https://github.com/steel-dev/steel-python/commit/a70bc614385d48955991218901b9ae8b0ca2ae1e)) +* **readme:** fix version rendering on pypi ([14b7820](https://github.com/steel-dev/steel-python/commit/14b7820aff48a061d6eeb74df86ec2e896d5e25c)) + ## 0.6.0 (2025-07-02) Full Changelog: [v0.5.0...v0.6.0](https://github.com/steel-dev/steel-python/compare/v0.5.0...v0.6.0) diff --git a/README.md b/README.md index c258968d..3da166a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Steel Python API library -[![PyPI version]()](https://pypi.org/project/steel-sdk/) + +[![PyPI version](https://img.shields.io/pypi/v/steel-sdk.svg?label=pypi%20(stable))](https://pypi.org/project/steel-sdk/) The Steel Python library provides convenient access to the Steel REST API from any Python 3.8+ application. The library includes type definitions for all request params and response fields, diff --git a/api.md b/api.md index 29586a1e..6c4d2e36 100644 --- a/api.md +++ b/api.md @@ -84,3 +84,16 @@ Methods: - client.sessions.files.download(path, \*, session_id) -> BinaryAPIResponse - client.sessions.files.download_archive(session_id) -> BinaryAPIResponse - client.sessions.files.upload(session_id, \*\*params) -> File + +## Captchas + +Types: + +```python +from steel.types.sessions import CaptchaSolveImageResponse, CaptchaStatusResponse +``` + +Methods: + +- client.sessions.captchas.solve_image(session_id, \*\*params) -> CaptchaSolveImageResponse +- client.sessions.captchas.status(session_id) -> CaptchaStatusResponse diff --git a/pyproject.toml b/pyproject.toml index 4a928087..144e5342 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "steel-sdk" -version = "0.6.0" +version = "0.7.0" description = "The official Python library for the steel API" dynamic = ["readme"] license = "Apache-2.0" @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", @@ -38,7 +39,7 @@ Homepage = "https://github.com/steel-dev/steel-python" Repository = "https://github.com/steel-dev/steel-python" [project.optional-dependencies] -aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.8"] [tool.rye] managed = true diff --git a/requirements-dev.lock b/requirements-dev.lock index 6738edc6..c0923eb9 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,15 +48,15 @@ filelock==3.12.4 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp # via respx # via steel-sdk -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via steel-sdk idna==3.4 # via anyio diff --git a/requirements.lock b/requirements.lock index 8a0fc2ec..55404de9 100644 --- a/requirements.lock +++ b/requirements.lock @@ -36,14 +36,14 @@ exceptiongroup==1.2.2 frozenlist==1.6.2 # via aiohttp # via aiosignal -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.9 # via httpx httpx==0.28.1 # via httpx-aiohttp # via steel-sdk -httpx-aiohttp==0.1.6 +httpx-aiohttp==0.1.8 # via steel-sdk idna==3.4 # via anyio diff --git a/src/steel/_base_client.py b/src/steel/_base_client.py index 74d7c57e..e1c26eba 100644 --- a/src/steel/_base_client.py +++ b/src/steel/_base_client.py @@ -529,6 +529,15 @@ def _build_request( # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + kwargs["json"] = json_data if is_given(json_data) else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + # TODO: report this error to httpx return self._client.build_request( # pyright: ignore[reportUnknownMemberType] headers=headers, @@ -540,8 +549,6 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data if is_given(json_data) else None, - files=files, **kwargs, ) diff --git a/src/steel/_models.py b/src/steel/_models.py index 4f214980..528d5680 100644 --- a/src/steel/_models.py +++ b/src/steel/_models.py @@ -2,9 +2,10 @@ import os import inspect -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast +from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast from datetime import date, datetime from typing_extensions import ( + List, Unpack, Literal, ClassVar, @@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object: if type_ is None: raise RuntimeError(f"Unexpected field type is None for {key}") - return construct_type(value=value, type_=type_) + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) def is_basemodel(type_: type) -> bool: @@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: return cast(_T, construct_type(value=value, type_=type_)) -def construct_type(*, value: object, type_: object) -> object: +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: """Loose coercion to the expected type with construction of nested values. If the given value does not match the expected type then it is returned as-is. @@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object: type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` - if is_annotated_type(type_): - meta: tuple[Any, ...] = get_args(type_)[1:] + if metadata is not None: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] type_ = extract_type_arg(type_, 0) else: meta = tuple() diff --git a/src/steel/_version.py b/src/steel/_version.py index 13f46648..07e5c9af 100644 --- a/src/steel/_version.py +++ b/src/steel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "steel" -__version__ = "0.6.0" # x-release-please-version +__version__ = "0.7.0" # x-release-please-version diff --git a/src/steel/resources/sessions/__init__.py b/src/steel/resources/sessions/__init__.py index 0863ab59..356a62b6 100644 --- a/src/steel/resources/sessions/__init__.py +++ b/src/steel/resources/sessions/__init__.py @@ -8,6 +8,14 @@ FilesResourceWithStreamingResponse, AsyncFilesResourceWithStreamingResponse, ) +from .captchas import ( + CaptchasResource, + AsyncCaptchasResource, + CaptchasResourceWithRawResponse, + AsyncCaptchasResourceWithRawResponse, + CaptchasResourceWithStreamingResponse, + AsyncCaptchasResourceWithStreamingResponse, +) from .sessions import ( SessionsResource, AsyncSessionsResource, @@ -24,6 +32,12 @@ "AsyncFilesResourceWithRawResponse", "FilesResourceWithStreamingResponse", "AsyncFilesResourceWithStreamingResponse", + "CaptchasResource", + "AsyncCaptchasResource", + "CaptchasResourceWithRawResponse", + "AsyncCaptchasResourceWithRawResponse", + "CaptchasResourceWithStreamingResponse", + "AsyncCaptchasResourceWithStreamingResponse", "SessionsResource", "AsyncSessionsResource", "SessionsResourceWithRawResponse", diff --git a/src/steel/resources/sessions/captchas.py b/src/steel/resources/sessions/captchas.py new file mode 100644 index 00000000..ab7010d4 --- /dev/null +++ b/src/steel/resources/sessions/captchas.py @@ -0,0 +1,278 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.sessions import captcha_solve_image_params +from ...types.sessions.captcha_status_response import CaptchaStatusResponse +from ...types.sessions.captcha_solve_image_response import CaptchaSolveImageResponse + +__all__ = ["CaptchasResource", "AsyncCaptchasResource"] + + +class CaptchasResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> CaptchasResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/steel-dev/steel-python#accessing-raw-response-data-eg-headers + """ + return CaptchasResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CaptchasResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/steel-dev/steel-python#with_streaming_response + """ + return CaptchasResourceWithStreamingResponse(self) + + def solve_image( + self, + session_id: str, + *, + image_x_path: str, + input_x_path: str, + url: str | NotGiven = NOT_GIVEN, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaptchaSolveImageResponse: + """ + Solves an image captcha using XPath selectors + + Args: + image_x_path: XPath to the captcha image element + + input_x_path: XPath to the captcha input element + + url: URL where the captcha is located + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return self._post( + f"/v1/sessions/{session_id}/captchas/solve-image", + body=maybe_transform( + { + "image_x_path": image_x_path, + "input_x_path": input_x_path, + "url": url, + }, + captcha_solve_image_params.CaptchaSolveImageParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CaptchaSolveImageResponse, + ) + + def status( + self, + session_id: str, + *, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaptchaStatusResponse: + """ + Gets the current captcha status for a session + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return self._get( + f"/v1/sessions/{session_id}/captchas/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CaptchaStatusResponse, + ) + + +class AsyncCaptchasResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncCaptchasResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/steel-dev/steel-python#accessing-raw-response-data-eg-headers + """ + return AsyncCaptchasResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCaptchasResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/steel-dev/steel-python#with_streaming_response + """ + return AsyncCaptchasResourceWithStreamingResponse(self) + + async def solve_image( + self, + session_id: str, + *, + image_x_path: str, + input_x_path: str, + url: str | NotGiven = NOT_GIVEN, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaptchaSolveImageResponse: + """ + Solves an image captcha using XPath selectors + + Args: + image_x_path: XPath to the captcha image element + + input_x_path: XPath to the captcha input element + + url: URL where the captcha is located + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return await self._post( + f"/v1/sessions/{session_id}/captchas/solve-image", + body=await async_maybe_transform( + { + "image_x_path": image_x_path, + "input_x_path": input_x_path, + "url": url, + }, + captcha_solve_image_params.CaptchaSolveImageParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CaptchaSolveImageResponse, + ) + + async def status( + self, + session_id: str, + *, + # 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaptchaStatusResponse: + """ + Gets the current captcha status for a session + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return await self._get( + f"/v1/sessions/{session_id}/captchas/status", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CaptchaStatusResponse, + ) + + +class CaptchasResourceWithRawResponse: + def __init__(self, captchas: CaptchasResource) -> None: + self._captchas = captchas + + self.solve_image = to_raw_response_wrapper( + captchas.solve_image, + ) + self.status = to_raw_response_wrapper( + captchas.status, + ) + + +class AsyncCaptchasResourceWithRawResponse: + def __init__(self, captchas: AsyncCaptchasResource) -> None: + self._captchas = captchas + + self.solve_image = async_to_raw_response_wrapper( + captchas.solve_image, + ) + self.status = async_to_raw_response_wrapper( + captchas.status, + ) + + +class CaptchasResourceWithStreamingResponse: + def __init__(self, captchas: CaptchasResource) -> None: + self._captchas = captchas + + self.solve_image = to_streamed_response_wrapper( + captchas.solve_image, + ) + self.status = to_streamed_response_wrapper( + captchas.status, + ) + + +class AsyncCaptchasResourceWithStreamingResponse: + def __init__(self, captchas: AsyncCaptchasResource) -> None: + self._captchas = captchas + + self.solve_image = async_to_streamed_response_wrapper( + captchas.solve_image, + ) + self.status = async_to_streamed_response_wrapper( + captchas.status, + ) diff --git a/src/steel/resources/sessions/sessions.py b/src/steel/resources/sessions/sessions.py index 5b9c54e1..96d6f5da 100644 --- a/src/steel/resources/sessions/sessions.py +++ b/src/steel/resources/sessions/sessions.py @@ -17,6 +17,14 @@ from ...types import session_list_params, session_create_params from ..._types import NOT_GIVEN, Body, Query, Headers, NotGiven from ..._utils import maybe_transform, async_maybe_transform +from .captchas import ( + CaptchasResource, + AsyncCaptchasResource, + CaptchasResourceWithRawResponse, + AsyncCaptchasResourceWithRawResponse, + CaptchasResourceWithStreamingResponse, + AsyncCaptchasResourceWithStreamingResponse, +) from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -43,6 +51,10 @@ class SessionsResource(SyncAPIResource): def files(self) -> FilesResource: return FilesResource(self._client) + @cached_property + def captchas(self) -> CaptchasResource: + return CaptchasResource(self._client) + @cached_property def with_raw_response(self) -> SessionsResourceWithRawResponse: """ @@ -403,6 +415,10 @@ class AsyncSessionsResource(AsyncAPIResource): def files(self) -> AsyncFilesResource: return AsyncFilesResource(self._client) + @cached_property + def captchas(self) -> AsyncCaptchasResource: + return AsyncCaptchasResource(self._client) + @cached_property def with_raw_response(self) -> AsyncSessionsResourceWithRawResponse: """ @@ -791,6 +807,10 @@ def __init__(self, sessions: SessionsResource) -> None: def files(self) -> FilesResourceWithRawResponse: return FilesResourceWithRawResponse(self._sessions.files) + @cached_property + def captchas(self) -> CaptchasResourceWithRawResponse: + return CaptchasResourceWithRawResponse(self._sessions.captchas) + class AsyncSessionsResourceWithRawResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: @@ -825,6 +845,10 @@ def __init__(self, sessions: AsyncSessionsResource) -> None: def files(self) -> AsyncFilesResourceWithRawResponse: return AsyncFilesResourceWithRawResponse(self._sessions.files) + @cached_property + def captchas(self) -> AsyncCaptchasResourceWithRawResponse: + return AsyncCaptchasResourceWithRawResponse(self._sessions.captchas) + class SessionsResourceWithStreamingResponse: def __init__(self, sessions: SessionsResource) -> None: @@ -859,6 +883,10 @@ def __init__(self, sessions: SessionsResource) -> None: def files(self) -> FilesResourceWithStreamingResponse: return FilesResourceWithStreamingResponse(self._sessions.files) + @cached_property + def captchas(self) -> CaptchasResourceWithStreamingResponse: + return CaptchasResourceWithStreamingResponse(self._sessions.captchas) + class AsyncSessionsResourceWithStreamingResponse: def __init__(self, sessions: AsyncSessionsResource) -> None: @@ -892,3 +920,7 @@ def __init__(self, sessions: AsyncSessionsResource) -> None: @cached_property def files(self) -> AsyncFilesResourceWithStreamingResponse: return AsyncFilesResourceWithStreamingResponse(self._sessions.files) + + @cached_property + def captchas(self) -> AsyncCaptchasResourceWithStreamingResponse: + return AsyncCaptchasResourceWithStreamingResponse(self._sessions.captchas) diff --git a/src/steel/types/sessions/__init__.py b/src/steel/types/sessions/__init__.py index d390deb5..fddfdee9 100644 --- a/src/steel/types/sessions/__init__.py +++ b/src/steel/types/sessions/__init__.py @@ -3,3 +3,6 @@ from __future__ import annotations from .file_upload_params import FileUploadParams as FileUploadParams +from .captcha_status_response import CaptchaStatusResponse as CaptchaStatusResponse +from .captcha_solve_image_params import CaptchaSolveImageParams as CaptchaSolveImageParams +from .captcha_solve_image_response import CaptchaSolveImageResponse as CaptchaSolveImageResponse diff --git a/src/steel/types/sessions/captcha_solve_image_params.py b/src/steel/types/sessions/captcha_solve_image_params.py new file mode 100644 index 00000000..12ecf516 --- /dev/null +++ b/src/steel/types/sessions/captcha_solve_image_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["CaptchaSolveImageParams"] + + +class CaptchaSolveImageParams(TypedDict, total=False): + image_x_path: Required[Annotated[str, PropertyInfo(alias="imageXPath")]] + """XPath to the captcha image element""" + + input_x_path: Required[Annotated[str, PropertyInfo(alias="inputXPath")]] + """XPath to the captcha input element""" + + url: str + """URL where the captcha is located""" diff --git a/src/steel/types/sessions/captcha_solve_image_response.py b/src/steel/types/sessions/captcha_solve_image_response.py new file mode 100644 index 00000000..fa5dd47b --- /dev/null +++ b/src/steel/types/sessions/captcha_solve_image_response.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["CaptchaSolveImageResponse"] + + +class CaptchaSolveImageResponse(BaseModel): + success: bool + """Whether the action was successful""" + + broadcasted: Optional[bool] = None + """Whether the message was broadcasted""" + + message: Optional[str] = None + """Response message""" + + requested: Optional[bool] = None + """Whether the state was requested""" diff --git a/src/steel/types/sessions/captcha_status_response.py b/src/steel/types/sessions/captcha_status_response.py new file mode 100644 index 00000000..eca5cc6d --- /dev/null +++ b/src/steel/types/sessions/captcha_status_response.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import TypeAlias + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["CaptchaStatusResponse", "CaptchaStatusResponseItem"] + + +class CaptchaStatusResponseItem(BaseModel): + is_solving_captcha: bool = FieldInfo(alias="isSolvingCaptcha") + """Whether a captcha is currently being solved""" + + page_id: str = FieldInfo(alias="pageId") + """The page ID where the captcha is located""" + + tasks: List[object] + """Array of captcha tasks""" + + url: str + """The URL where the captcha is located""" + + created: Optional[float] = None + """Timestamp when the state was created""" + + last_updated: Optional[float] = FieldInfo(alias="lastUpdated", default=None) + """Timestamp when the state was last updated""" + + +CaptchaStatusResponse: TypeAlias = List[CaptchaStatusResponseItem] diff --git a/tests/api_resources/sessions/test_captchas.py b/tests/api_resources/sessions/test_captchas.py new file mode 100644 index 00000000..5b9ebdbd --- /dev/null +++ b/tests/api_resources/sessions/test_captchas.py @@ -0,0 +1,212 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from steel import Steel, AsyncSteel +from tests.utils import assert_matches_type +from steel.types.sessions import CaptchaStatusResponse, CaptchaSolveImageResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestCaptchas: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_solve_image(self, client: Steel) -> None: + captcha = client.sessions.captchas.solve_image( + session_id="sessionId", + image_x_path="imageXPath", + input_x_path="inputXPath", + ) + assert_matches_type(CaptchaSolveImageResponse, captcha, path=["response"]) + + @parametrize + def test_method_solve_image_with_all_params(self, client: Steel) -> None: + captcha = client.sessions.captchas.solve_image( + session_id="sessionId", + image_x_path="imageXPath", + input_x_path="inputXPath", + url="url", + ) + assert_matches_type(CaptchaSolveImageResponse, captcha, path=["response"]) + + @parametrize + def test_raw_response_solve_image(self, client: Steel) -> None: + response = client.sessions.captchas.with_raw_response.solve_image( + session_id="sessionId", + image_x_path="imageXPath", + input_x_path="inputXPath", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + captcha = response.parse() + assert_matches_type(CaptchaSolveImageResponse, captcha, path=["response"]) + + @parametrize + def test_streaming_response_solve_image(self, client: Steel) -> None: + with client.sessions.captchas.with_streaming_response.solve_image( + session_id="sessionId", + image_x_path="imageXPath", + input_x_path="inputXPath", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + captcha = response.parse() + assert_matches_type(CaptchaSolveImageResponse, captcha, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_solve_image(self, client: Steel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.sessions.captchas.with_raw_response.solve_image( + session_id="", + image_x_path="imageXPath", + input_x_path="inputXPath", + ) + + @parametrize + def test_method_status(self, client: Steel) -> None: + captcha = client.sessions.captchas.status( + "sessionId", + ) + assert_matches_type(CaptchaStatusResponse, captcha, path=["response"]) + + @parametrize + def test_raw_response_status(self, client: Steel) -> None: + response = client.sessions.captchas.with_raw_response.status( + "sessionId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + captcha = response.parse() + assert_matches_type(CaptchaStatusResponse, captcha, path=["response"]) + + @parametrize + def test_streaming_response_status(self, client: Steel) -> None: + with client.sessions.captchas.with_streaming_response.status( + "sessionId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + captcha = response.parse() + assert_matches_type(CaptchaStatusResponse, captcha, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_status(self, client: Steel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + client.sessions.captchas.with_raw_response.status( + "", + ) + + +class TestAsyncCaptchas: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_solve_image(self, async_client: AsyncSteel) -> None: + captcha = await async_client.sessions.captchas.solve_image( + session_id="sessionId", + image_x_path="imageXPath", + input_x_path="inputXPath", + ) + assert_matches_type(CaptchaSolveImageResponse, captcha, path=["response"]) + + @parametrize + async def test_method_solve_image_with_all_params(self, async_client: AsyncSteel) -> None: + captcha = await async_client.sessions.captchas.solve_image( + session_id="sessionId", + image_x_path="imageXPath", + input_x_path="inputXPath", + url="url", + ) + assert_matches_type(CaptchaSolveImageResponse, captcha, path=["response"]) + + @parametrize + async def test_raw_response_solve_image(self, async_client: AsyncSteel) -> None: + response = await async_client.sessions.captchas.with_raw_response.solve_image( + session_id="sessionId", + image_x_path="imageXPath", + input_x_path="inputXPath", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + captcha = await response.parse() + assert_matches_type(CaptchaSolveImageResponse, captcha, path=["response"]) + + @parametrize + async def test_streaming_response_solve_image(self, async_client: AsyncSteel) -> None: + async with async_client.sessions.captchas.with_streaming_response.solve_image( + session_id="sessionId", + image_x_path="imageXPath", + input_x_path="inputXPath", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + captcha = await response.parse() + assert_matches_type(CaptchaSolveImageResponse, captcha, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_solve_image(self, async_client: AsyncSteel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.sessions.captchas.with_raw_response.solve_image( + session_id="", + image_x_path="imageXPath", + input_x_path="inputXPath", + ) + + @parametrize + async def test_method_status(self, async_client: AsyncSteel) -> None: + captcha = await async_client.sessions.captchas.status( + "sessionId", + ) + assert_matches_type(CaptchaStatusResponse, captcha, path=["response"]) + + @parametrize + async def test_raw_response_status(self, async_client: AsyncSteel) -> None: + response = await async_client.sessions.captchas.with_raw_response.status( + "sessionId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + captcha = await response.parse() + assert_matches_type(CaptchaStatusResponse, captcha, path=["response"]) + + @parametrize + async def test_streaming_response_status(self, async_client: AsyncSteel) -> None: + async with async_client.sessions.captchas.with_streaming_response.status( + "sessionId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + captcha = await response.parse() + assert_matches_type(CaptchaStatusResponse, captcha, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_status(self, async_client: AsyncSteel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): + await async_client.sessions.captchas.with_raw_response.status( + "", + ) diff --git a/tests/test_client.py b/tests/test_client.py index 0c16cdb5..9643fd53 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -427,7 +427,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, client: Steel) -> None: request = client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, @@ -1187,7 +1187,7 @@ def test_request_extra_query(self) -> None: def test_multipart_repeating_array(self, async_client: AsyncSteel) -> None: request = async_client._build_request( FinalRequestOptions.construct( - method="get", + method="post", url="/foo", headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, json_data={"array": ["foo", "bar"]}, diff --git a/tests/test_models.py b/tests/test_models.py index 27133822..be6d8290 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -889,3 +889,48 @@ class ModelB(BaseModel): ) assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2)