From 941b1cc3019655d765bc4e1d66a8d5eb7c66b699 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 05:42:15 +0000 Subject: [PATCH 01/15] fix(types): allow pyright to infer TypedDict types within SequenceNotStr --- src/steel/_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/steel/_types.py b/src/steel/_types.py index 4a0df7c..331c956 100644 --- a/src/steel/_types.py +++ b/src/steel/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case From f16212dbd7d11e72e909b4c527358e4c6577d342 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 05:43:59 +0000 Subject: [PATCH 02/15] chore: add missing docstrings --- src/steel/types/extension_list_response.py | 2 ++ src/steel/types/profile_create_params.py | 2 ++ src/steel/types/profile_create_response.py | 4 +++ src/steel/types/profile_list_response.py | 4 +++ src/steel/types/scrape_response.py | 2 ++ src/steel/types/session.py | 17 +++++++++++++ src/steel/types/session_context.py | 4 +++ src/steel/types/session_create_params.py | 25 +++++++++++++++++++ .../types/session_release_all_response.py | 2 ++ src/steel/types/session_release_response.py | 2 ++ src/steel/types/sessionslist.py | 19 ++++++++++++++ 11 files changed, 83 insertions(+) diff --git a/src/steel/types/extension_list_response.py b/src/steel/types/extension_list_response.py index bc6c99a..0608588 100644 --- a/src/steel/types/extension_list_response.py +++ b/src/steel/types/extension_list_response.py @@ -24,6 +24,8 @@ class Extension(BaseModel): class ExtensionListResponse(BaseModel): + """Response containing a list of extensions for the organization""" + count: float """Total number of extensions""" diff --git a/src/steel/types/profile_create_params.py b/src/steel/types/profile_create_params.py index eb20bef..f0858a8 100644 --- a/src/steel/types/profile_create_params.py +++ b/src/steel/types/profile_create_params.py @@ -24,6 +24,8 @@ class ProfileCreateParams(TypedDict, total=False): class Dimensions(TypedDict, total=False): + """The dimensions associated with the profile""" + height: Required[float] width: Required[float] diff --git a/src/steel/types/profile_create_response.py b/src/steel/types/profile_create_response.py index 28561db..4ca0e7c 100644 --- a/src/steel/types/profile_create_response.py +++ b/src/steel/types/profile_create_response.py @@ -32,6 +32,8 @@ class Dimensions(BaseModel): + """The dimensions associated with the profile""" + height: float width: float @@ -466,6 +468,8 @@ def __getattr__(self, attr: str) -> object: ... class Fingerprint(BaseModel): + """The fingerprint associated with the profile""" + fingerprint: FingerprintFingerprint headers: FingerprintHeaders diff --git a/src/steel/types/profile_list_response.py b/src/steel/types/profile_list_response.py index 9c6a445..cecb348 100644 --- a/src/steel/types/profile_list_response.py +++ b/src/steel/types/profile_list_response.py @@ -33,6 +33,8 @@ class ProfileDimensions(BaseModel): + """The dimensions associated with the profile""" + height: float width: float @@ -467,6 +469,8 @@ def __getattr__(self, attr: str) -> object: ... class ProfileFingerprint(BaseModel): + """The fingerprint associated with the profile""" + fingerprint: ProfileFingerprintFingerprint headers: ProfileFingerprintHeaders diff --git a/src/steel/types/scrape_response.py b/src/steel/types/scrape_response.py index cf9482b..a15bb25 100644 --- a/src/steel/types/scrape_response.py +++ b/src/steel/types/scrape_response.py @@ -102,6 +102,8 @@ class Screenshot(BaseModel): class ScrapeResponse(BaseModel): + """Response from a successful scrape request""" + content: Content links: List[Link] diff --git a/src/steel/types/session.py b/src/steel/types/session.py index 6d59145..5a13073 100644 --- a/src/steel/types/session.py +++ b/src/steel/types/session.py @@ -12,6 +12,8 @@ class Dimensions(BaseModel): + """Viewport and browser window dimensions for the session""" + height: int """Height of the browser window""" @@ -20,6 +22,8 @@ class Dimensions(BaseModel): class OptimizeBandwidth(BaseModel): + """Bandwidth optimizations that were applied to the session.""" + block_hosts: Optional[List[str]] = FieldInfo(alias="blockHosts", default=None) block_images: Optional[bool] = FieldInfo(alias="blockImages", default=None) @@ -32,6 +36,11 @@ class OptimizeBandwidth(BaseModel): class DebugConfig(BaseModel): + """Configuration for the debug URL and session viewer. + + Controls interaction capabilities and cursor visibility. + """ + interactive: Optional[bool] = None """Whether interaction is allowed via the debug URL viewer. @@ -46,10 +55,14 @@ class DebugConfig(BaseModel): class DeviceConfig(BaseModel): + """Device configuration for the session""" + device: Optional[Literal["desktop", "mobile"]] = None class StealthConfig(BaseModel): + """Stealth configuration for the session""" + humanize_interactions: Optional[bool] = FieldInfo(alias="humanizeInteractions", default=None) """ This flag will make the browser act more human-like by moving the mouse in a @@ -61,6 +74,10 @@ class StealthConfig(BaseModel): class Session(BaseModel): + """ + Represents the data structure for a browser session, including its configuration and status. + """ + id: str """Unique identifier for the session""" diff --git a/src/steel/types/session_context.py b/src/steel/types/session_context.py index 445867e..dc522c7 100644 --- a/src/steel/types/session_context.py +++ b/src/steel/types/session_context.py @@ -20,6 +20,8 @@ class CookiePartitionKey(BaseModel): + """The partition key of the cookie""" + has_cross_site_ancestor: bool = FieldInfo(alias="hasCrossSiteAncestor") """ Indicates if the cookie has any ancestors that are cross-site to the @@ -122,6 +124,8 @@ class IndexedDB(BaseModel): class SessionContext(BaseModel): + """Session context data returned from a browser session.""" + cookies: Optional[List[Cookie]] = None """Cookies to initialize in the session""" diff --git a/src/steel/types/session_create_params.py b/src/steel/types/session_create_params.py index 3c4c6cb..ca651c4 100644 --- a/src/steel/types/session_create_params.py +++ b/src/steel/types/session_create_params.py @@ -131,6 +131,8 @@ class SessionCreateParams(TypedDict, total=False): class Credentials(TypedDict, total=False): + """Configuration for session credentials""" + auto_submit: Annotated[bool, PropertyInfo(alias="autoSubmit")] blur_fields: Annotated[bool, PropertyInfo(alias="blurFields")] @@ -139,6 +141,11 @@ class Credentials(TypedDict, total=False): class DebugConfig(TypedDict, total=False): + """Configuration for the debug URL and session viewer. + + Controls interaction capabilities, cursor visibility, and other debug-related settings. + """ + interactive: bool """Allow interaction with the browser session via the debug URL viewer. @@ -154,10 +161,17 @@ class DebugConfig(TypedDict, total=False): class DeviceConfig(TypedDict, total=False): + """Device configuration for the session. + + Specify 'mobile' for mobile device fingerprints and configurations. + """ + device: Literal["desktop", "mobile"] class Dimensions(TypedDict, total=False): + """Viewport and browser window dimensions for the session""" + height: Required[int] """Height of the session""" @@ -181,6 +195,8 @@ class OptimizeBandwidthUnionMember1(TypedDict, total=False): class SessionContextCookiePartitionKey(TypedDict, total=False): + """The partition key of the cookie""" + has_cross_site_ancestor: Required[Annotated[bool, PropertyInfo(alias="hasCrossSiteAncestor")]] """ Indicates if the cookie has any ancestors that are cross-site to the @@ -283,6 +299,11 @@ class SessionContextIndexedDB(TypedDict, total=False): class SessionContext(TypedDict, total=False): + """Session context data to be used in the created session. + + Sessions will start with an empty context by default. + """ + cookies: Iterable[SessionContextCookie] """Cookies to initialize in the session""" @@ -297,6 +318,8 @@ class SessionContext(TypedDict, total=False): class StealthConfig(TypedDict, total=False): + """Stealth configuration for the session""" + humanize_interactions: Annotated[bool, PropertyInfo(alias="humanizeInteractions")] """ This flag will make the browser act more human-like by moving the mouse in a @@ -308,6 +331,8 @@ class StealthConfig(TypedDict, total=False): class UseProxyGeolocationGeolocation(TypedDict, total=False): + """Geographic location for the proxy""" + country: Required[ Literal[ "US", diff --git a/src/steel/types/session_release_all_response.py b/src/steel/types/session_release_all_response.py index 22084df..1dfdfe3 100644 --- a/src/steel/types/session_release_all_response.py +++ b/src/steel/types/session_release_all_response.py @@ -6,6 +6,8 @@ class SessionReleaseAllResponse(BaseModel): + """Response for releasing multiple sessions.""" + message: str """Details about the outcome of the release operation""" diff --git a/src/steel/types/session_release_response.py b/src/steel/types/session_release_response.py index ce31b8e..08bbc14 100644 --- a/src/steel/types/session_release_response.py +++ b/src/steel/types/session_release_response.py @@ -6,6 +6,8 @@ class SessionReleaseResponse(BaseModel): + """Response for releasing a single session.""" + message: str """Details about the outcome of the release operation""" diff --git a/src/steel/types/sessionslist.py b/src/steel/types/sessionslist.py index 53833ed..7a451a8 100644 --- a/src/steel/types/sessionslist.py +++ b/src/steel/types/sessionslist.py @@ -20,6 +20,8 @@ class SessionDimensions(BaseModel): + """Viewport and browser window dimensions for the session""" + height: int """Height of the browser window""" @@ -28,6 +30,8 @@ class SessionDimensions(BaseModel): class SessionOptimizeBandwidth(BaseModel): + """Bandwidth optimizations that were applied to the session.""" + block_hosts: Optional[List[str]] = FieldInfo(alias="blockHosts", default=None) block_images: Optional[bool] = FieldInfo(alias="blockImages", default=None) @@ -40,6 +44,11 @@ class SessionOptimizeBandwidth(BaseModel): class SessionDebugConfig(BaseModel): + """Configuration for the debug URL and session viewer. + + Controls interaction capabilities and cursor visibility. + """ + interactive: Optional[bool] = None """Whether interaction is allowed via the debug URL viewer. @@ -54,10 +63,14 @@ class SessionDebugConfig(BaseModel): class SessionDeviceConfig(BaseModel): + """Device configuration for the session""" + device: Optional[Literal["desktop", "mobile"]] = None class SessionStealthConfig(BaseModel): + """Stealth configuration for the session""" + humanize_interactions: Optional[bool] = FieldInfo(alias="humanizeInteractions", default=None) """ This flag will make the browser act more human-like by moving the mouse in a @@ -69,6 +82,10 @@ class SessionStealthConfig(BaseModel): class Session(BaseModel): + """ + Represents the data structure for a browser session, including its configuration and status. + """ + id: str """Unique identifier for the session""" @@ -146,5 +163,7 @@ class Session(BaseModel): class Sessionslist(BaseModel): + """Response containing a list of browser sessions with pagination details.""" + sessions: List[Session] """List of browser sessions""" From 0bed812d3834d067c21cd3c5d839c8f6abc8ec8a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:40:07 +0000 Subject: [PATCH 03/15] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 419210f..a6c9f08 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 36 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-aa04a4c92f7068c304082be35f7b4c1474d12772ab9756e0a3819db9f14e1063.yml -openapi_spec_hash: 19b08d835a276c527167b13b86225ae1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-6f8dc0942cdc81f2adc752ae5b24748e288452f8bfac26d7b64143c57fb999c0.yml +openapi_spec_hash: 809a6df32a171c658fa792064093e1d8 config_hash: e49b3f69d57d7ffa0420acf772d5c846 From 92b8e72a9530757a3da642154a0c9d6e00f158b8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 05:22:04 +0000 Subject: [PATCH 04/15] chore(internal): add missing files argument to base client --- src/steel/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/steel/_base_client.py b/src/steel/_base_client.py index c03de41..4d35d3c 100644 --- a/src/steel/_base_client.py +++ b/src/steel/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From cb228f5f13404ed892accf61ecece2d4a62a6670 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 08:10:22 +0000 Subject: [PATCH 05/15] chore: speedup initial import --- src/steel/_client.py | 269 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 216 insertions(+), 53 deletions(-) diff --git a/src/steel/_client.py b/src/steel/_client.py index 9ceee7e..e23f4c3 100644 --- a/src/steel/_client.py +++ b/src/steel/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, List, Mapping +from typing import TYPE_CHECKING, Any, List, Mapping from typing_extensions import Self, Literal, override import httpx @@ -30,6 +30,7 @@ get_async_library, async_maybe_transform, ) +from ._compat import cached_property from ._version import __version__ from ._response import ( to_raw_response_wrapper, @@ -37,7 +38,6 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .resources import files, profiles, extensions, credentials from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import APIStatusError from ._base_client import ( @@ -46,23 +46,22 @@ AsyncAPIClient, make_request_options, ) -from .resources.sessions import sessions from .types.pdf_response import PdfResponse from .types.scrape_response import ScrapeResponse from .types.screenshot_response import ScreenshotResponse +if TYPE_CHECKING: + from .resources import files, profiles, sessions, extensions, credentials + from .resources.files import FilesResource, AsyncFilesResource + from .resources.profiles import ProfilesResource, AsyncProfilesResource + from .resources.extensions import ExtensionsResource, AsyncExtensionsResource + from .resources.credentials import CredentialsResource, AsyncCredentialsResource + from .resources.sessions.sessions import SessionsResource, AsyncSessionsResource + __all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Steel", "AsyncSteel", "Client", "AsyncClient"] class Steel(SyncAPIClient): - credentials: credentials.CredentialsResource - files: files.FilesResource - sessions: sessions.SessionsResource - extensions: extensions.ExtensionsResource - profiles: profiles.ProfilesResource - with_raw_response: SteelWithRawResponse - with_streaming_response: SteelWithStreamedResponse - # client options steel_api_key: str | None @@ -113,13 +112,43 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.credentials = credentials.CredentialsResource(self) - self.files = files.FilesResource(self) - self.sessions = sessions.SessionsResource(self) - self.extensions = extensions.ExtensionsResource(self) - self.profiles = profiles.ProfilesResource(self) - self.with_raw_response = SteelWithRawResponse(self) - self.with_streaming_response = SteelWithStreamedResponse(self) + @cached_property + def credentials(self) -> CredentialsResource: + from .resources.credentials import CredentialsResource + + return CredentialsResource(self) + + @cached_property + def files(self) -> FilesResource: + from .resources.files import FilesResource + + return FilesResource(self) + + @cached_property + def sessions(self) -> SessionsResource: + from .resources.sessions import SessionsResource + + return SessionsResource(self) + + @cached_property + def extensions(self) -> ExtensionsResource: + from .resources.extensions import ExtensionsResource + + return ExtensionsResource(self) + + @cached_property + def profiles(self) -> ProfilesResource: + from .resources.profiles import ProfilesResource + + return ProfilesResource(self) + + @cached_property + def with_raw_response(self) -> SteelWithRawResponse: + return SteelWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SteelWithStreamedResponse: + return SteelWithStreamedResponse(self) @property @override @@ -398,14 +427,6 @@ def _make_status_error( class AsyncSteel(AsyncAPIClient): - credentials: credentials.AsyncCredentialsResource - files: files.AsyncFilesResource - sessions: sessions.AsyncSessionsResource - extensions: extensions.AsyncExtensionsResource - profiles: profiles.AsyncProfilesResource - with_raw_response: AsyncSteelWithRawResponse - with_streaming_response: AsyncSteelWithStreamedResponse - # client options steel_api_key: str | None @@ -456,13 +477,43 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.credentials = credentials.AsyncCredentialsResource(self) - self.files = files.AsyncFilesResource(self) - self.sessions = sessions.AsyncSessionsResource(self) - self.extensions = extensions.AsyncExtensionsResource(self) - self.profiles = profiles.AsyncProfilesResource(self) - self.with_raw_response = AsyncSteelWithRawResponse(self) - self.with_streaming_response = AsyncSteelWithStreamedResponse(self) + @cached_property + def credentials(self) -> AsyncCredentialsResource: + from .resources.credentials import AsyncCredentialsResource + + return AsyncCredentialsResource(self) + + @cached_property + def files(self) -> AsyncFilesResource: + from .resources.files import AsyncFilesResource + + return AsyncFilesResource(self) + + @cached_property + def sessions(self) -> AsyncSessionsResource: + from .resources.sessions import AsyncSessionsResource + + return AsyncSessionsResource(self) + + @cached_property + def extensions(self) -> AsyncExtensionsResource: + from .resources.extensions import AsyncExtensionsResource + + return AsyncExtensionsResource(self) + + @cached_property + def profiles(self) -> AsyncProfilesResource: + from .resources.profiles import AsyncProfilesResource + + return AsyncProfilesResource(self) + + @cached_property + def with_raw_response(self) -> AsyncSteelWithRawResponse: + return AsyncSteelWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSteelWithStreamedResponse: + return AsyncSteelWithStreamedResponse(self) @property @override @@ -741,12 +792,10 @@ def _make_status_error( class SteelWithRawResponse: + _client: Steel + def __init__(self, client: Steel) -> None: - self.credentials = credentials.CredentialsResourceWithRawResponse(client.credentials) - self.files = files.FilesResourceWithRawResponse(client.files) - self.sessions = sessions.SessionsResourceWithRawResponse(client.sessions) - self.extensions = extensions.ExtensionsResourceWithRawResponse(client.extensions) - self.profiles = profiles.ProfilesResourceWithRawResponse(client.profiles) + self._client = client self.pdf = to_raw_response_wrapper( client.pdf, @@ -758,14 +807,42 @@ def __init__(self, client: Steel) -> None: client.screenshot, ) + @cached_property + def credentials(self) -> credentials.CredentialsResourceWithRawResponse: + from .resources.credentials import CredentialsResourceWithRawResponse + + return CredentialsResourceWithRawResponse(self._client.credentials) + + @cached_property + def files(self) -> files.FilesResourceWithRawResponse: + from .resources.files import FilesResourceWithRawResponse + + return FilesResourceWithRawResponse(self._client.files) + + @cached_property + def sessions(self) -> sessions.SessionsResourceWithRawResponse: + from .resources.sessions import SessionsResourceWithRawResponse + + return SessionsResourceWithRawResponse(self._client.sessions) + + @cached_property + def extensions(self) -> extensions.ExtensionsResourceWithRawResponse: + from .resources.extensions import ExtensionsResourceWithRawResponse + + return ExtensionsResourceWithRawResponse(self._client.extensions) + + @cached_property + def profiles(self) -> profiles.ProfilesResourceWithRawResponse: + from .resources.profiles import ProfilesResourceWithRawResponse + + return ProfilesResourceWithRawResponse(self._client.profiles) + class AsyncSteelWithRawResponse: + _client: AsyncSteel + def __init__(self, client: AsyncSteel) -> None: - self.credentials = credentials.AsyncCredentialsResourceWithRawResponse(client.credentials) - self.files = files.AsyncFilesResourceWithRawResponse(client.files) - self.sessions = sessions.AsyncSessionsResourceWithRawResponse(client.sessions) - self.extensions = extensions.AsyncExtensionsResourceWithRawResponse(client.extensions) - self.profiles = profiles.AsyncProfilesResourceWithRawResponse(client.profiles) + self._client = client self.pdf = async_to_raw_response_wrapper( client.pdf, @@ -777,14 +854,42 @@ def __init__(self, client: AsyncSteel) -> None: client.screenshot, ) + @cached_property + def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: + from .resources.credentials import AsyncCredentialsResourceWithRawResponse + + return AsyncCredentialsResourceWithRawResponse(self._client.credentials) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithRawResponse: + from .resources.files import AsyncFilesResourceWithRawResponse + + return AsyncFilesResourceWithRawResponse(self._client.files) + + @cached_property + def sessions(self) -> sessions.AsyncSessionsResourceWithRawResponse: + from .resources.sessions import AsyncSessionsResourceWithRawResponse + + return AsyncSessionsResourceWithRawResponse(self._client.sessions) + + @cached_property + def extensions(self) -> extensions.AsyncExtensionsResourceWithRawResponse: + from .resources.extensions import AsyncExtensionsResourceWithRawResponse + + return AsyncExtensionsResourceWithRawResponse(self._client.extensions) + + @cached_property + def profiles(self) -> profiles.AsyncProfilesResourceWithRawResponse: + from .resources.profiles import AsyncProfilesResourceWithRawResponse + + return AsyncProfilesResourceWithRawResponse(self._client.profiles) + class SteelWithStreamedResponse: + _client: Steel + def __init__(self, client: Steel) -> None: - self.credentials = credentials.CredentialsResourceWithStreamingResponse(client.credentials) - self.files = files.FilesResourceWithStreamingResponse(client.files) - self.sessions = sessions.SessionsResourceWithStreamingResponse(client.sessions) - self.extensions = extensions.ExtensionsResourceWithStreamingResponse(client.extensions) - self.profiles = profiles.ProfilesResourceWithStreamingResponse(client.profiles) + self._client = client self.pdf = to_streamed_response_wrapper( client.pdf, @@ -796,14 +901,42 @@ def __init__(self, client: Steel) -> None: client.screenshot, ) + @cached_property + def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: + from .resources.credentials import CredentialsResourceWithStreamingResponse + + return CredentialsResourceWithStreamingResponse(self._client.credentials) + + @cached_property + def files(self) -> files.FilesResourceWithStreamingResponse: + from .resources.files import FilesResourceWithStreamingResponse + + return FilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def sessions(self) -> sessions.SessionsResourceWithStreamingResponse: + from .resources.sessions import SessionsResourceWithStreamingResponse + + return SessionsResourceWithStreamingResponse(self._client.sessions) + + @cached_property + def extensions(self) -> extensions.ExtensionsResourceWithStreamingResponse: + from .resources.extensions import ExtensionsResourceWithStreamingResponse + + return ExtensionsResourceWithStreamingResponse(self._client.extensions) + + @cached_property + def profiles(self) -> profiles.ProfilesResourceWithStreamingResponse: + from .resources.profiles import ProfilesResourceWithStreamingResponse + + return ProfilesResourceWithStreamingResponse(self._client.profiles) + class AsyncSteelWithStreamedResponse: + _client: AsyncSteel + def __init__(self, client: AsyncSteel) -> None: - self.credentials = credentials.AsyncCredentialsResourceWithStreamingResponse(client.credentials) - self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) - self.sessions = sessions.AsyncSessionsResourceWithStreamingResponse(client.sessions) - self.extensions = extensions.AsyncExtensionsResourceWithStreamingResponse(client.extensions) - self.profiles = profiles.AsyncProfilesResourceWithStreamingResponse(client.profiles) + self._client = client self.pdf = async_to_streamed_response_wrapper( client.pdf, @@ -815,6 +948,36 @@ def __init__(self, client: AsyncSteel) -> None: client.screenshot, ) + @cached_property + def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingResponse: + from .resources.credentials import AsyncCredentialsResourceWithStreamingResponse + + return AsyncCredentialsResourceWithStreamingResponse(self._client.credentials) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithStreamingResponse: + from .resources.files import AsyncFilesResourceWithStreamingResponse + + return AsyncFilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def sessions(self) -> sessions.AsyncSessionsResourceWithStreamingResponse: + from .resources.sessions import AsyncSessionsResourceWithStreamingResponse + + return AsyncSessionsResourceWithStreamingResponse(self._client.sessions) + + @cached_property + def extensions(self) -> extensions.AsyncExtensionsResourceWithStreamingResponse: + from .resources.extensions import AsyncExtensionsResourceWithStreamingResponse + + return AsyncExtensionsResourceWithStreamingResponse(self._client.extensions) + + @cached_property + def profiles(self) -> profiles.AsyncProfilesResourceWithStreamingResponse: + from .resources.profiles import AsyncProfilesResourceWithStreamingResponse + + return AsyncProfilesResourceWithStreamingResponse(self._client.profiles) + Client = Steel From 4d5f789e363954650468371049d9d9a6822d4484 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:42:46 +0000 Subject: [PATCH 06/15] fix: use async_to_httpx_files in patch method --- src/steel/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steel/_base_client.py b/src/steel/_base_client.py index 4d35d3c..941f69d 100644 --- a/src/steel/_base_client.py +++ b/src/steel/_base_client.py @@ -1774,7 +1774,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From ed11e7aa7441a51314e4ca6b97489d89e34dc75a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 08:09:27 +0000 Subject: [PATCH 07/15] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index b596b6b..617c364 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import steel' From fa9cdd1963f4ef1b62c7043f6357152a1044ccf2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:03:05 +0000 Subject: [PATCH 08/15] chore(internal): codegen related update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 0b2f55c..b4425db 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Steel + Copyright 2026 Steel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 39bf14915d1a92a92bf999c7d4d7cdf457f594fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:55:15 +0000 Subject: [PATCH 09/15] feat(api): api update --- .stats.yml | 8 +- README.md | 17 + api.md | 9 +- src/steel/_client.py | 12 +- src/steel/_files.py | 2 +- src/steel/resources/extensions.py | 78 +-- src/steel/resources/files.py | 42 +- src/steel/resources/profiles.py | 272 +++++++++- src/steel/resources/sessions/files.py | 42 +- src/steel/resources/sessions/sessions.py | 14 +- src/steel/types/__init__.py | 3 + src/steel/types/client_pdf_params.py | 2 +- src/steel/types/client_scrape_params.py | 2 +- src/steel/types/client_screenshot_params.py | 2 +- src/steel/types/extension_update_params.py | 4 +- src/steel/types/extension_upload_params.py | 4 +- src/steel/types/file_upload_params.py | 6 +- src/steel/types/profile_create_params.py | 7 +- src/steel/types/profile_create_response.py | 14 +- src/steel/types/profile_get_response.py | 510 ++++++++++++++++++ src/steel/types/profile_list_response.py | 14 +- src/steel/types/profile_update_params.py | 32 ++ src/steel/types/profile_update_response.py | 510 ++++++++++++++++++ src/steel/types/session_context.py | 6 +- src/steel/types/session_create_params.py | 13 +- src/steel/types/session_list_params.py | 2 +- .../types/sessions/file_upload_params.py | 6 +- src/steel/types/sessionslist.py | 6 + tests/api_resources/sessions/test_files.py | 12 +- tests/api_resources/test_client.py | 12 +- tests/api_resources/test_extensions.py | 8 +- tests/api_resources/test_files.py | 28 +- tests/api_resources/test_profiles.py | 223 +++++++- tests/api_resources/test_sessions.py | 32 +- 34 files changed, 1754 insertions(+), 200 deletions(-) create mode 100644 src/steel/types/profile_get_response.py create mode 100644 src/steel/types/profile_update_params.py create mode 100644 src/steel/types/profile_update_response.py diff --git a/.stats.yml b/.stats.yml index a6c9f08..d133400 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 36 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-6f8dc0942cdc81f2adc752ae5b24748e288452f8bfac26d7b64143c57fb999c0.yml -openapi_spec_hash: 809a6df32a171c658fa792064093e1d8 -config_hash: e49b3f69d57d7ffa0420acf772d5c846 +configured_endpoints: 38 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-97dcad9b050e2818332d548e93c2882fa5482df4dca384ce7f8d3cabdfd92b69.yml +openapi_spec_hash: 8d3fa51c0740046e4caf2a299ba9d89e +config_hash: 03d8eae69b6431c0ae0566dddaa9b0ea diff --git a/README.md b/README.md index ed01021..17b65e6 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,23 @@ session = client.sessions.create( print(session.credentials) ``` +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from steel import Steel + +client = Steel() + +client.files.upload( + file=Path("/path/to/file"), +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `steel.APIConnectionError` is raised. diff --git a/api.md b/api.md index 719847e..04ba9bb 100644 --- a/api.md +++ b/api.md @@ -129,10 +129,17 @@ Methods: Types: ```python -from steel.types import ProfileCreateResponse, ProfileListResponse +from steel.types import ( + ProfileCreateResponse, + ProfileUpdateResponse, + ProfileListResponse, + ProfileGetResponse, +) ``` Methods: - client.profiles.create(\*\*params) -> ProfileCreateResponse +- client.profiles.update(id, \*\*params) -> ProfileUpdateResponse - client.profiles.list() -> ProfileListResponse +- client.profiles.get(id) -> ProfileGetResponse diff --git a/src/steel/_client.py b/src/steel/_client.py index e23f4c3..eb20451 100644 --- a/src/steel/_client.py +++ b/src/steel/_client.py @@ -228,7 +228,7 @@ def pdf( *, url: str, delay: float | Omit = omit, - region: str | Omit = omit, + region: object | Omit = omit, use_proxy: bool | Omit = omit, # 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. @@ -281,7 +281,7 @@ def scrape( delay: float | Omit = omit, format: List[Literal["html", "readability", "cleaned_html", "markdown"]] | Omit = omit, pdf: bool | Omit = omit, - region: str | Omit = omit, + region: object | Omit = omit, screenshot: bool | Omit = omit, use_proxy: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -343,7 +343,7 @@ def screenshot( url: str, delay: float | Omit = omit, full_page: bool | Omit = omit, - region: str | Omit = omit, + region: object | Omit = omit, use_proxy: bool | Omit = omit, # 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. @@ -593,7 +593,7 @@ async def pdf( *, url: str, delay: float | Omit = omit, - region: str | Omit = omit, + region: object | Omit = omit, use_proxy: bool | Omit = omit, # 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. @@ -646,7 +646,7 @@ async def scrape( delay: float | Omit = omit, format: List[Literal["html", "readability", "cleaned_html", "markdown"]] | Omit = omit, pdf: bool | Omit = omit, - region: str | Omit = omit, + region: object | Omit = omit, screenshot: bool | Omit = omit, use_proxy: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -708,7 +708,7 @@ async def screenshot( url: str, delay: float | Omit = omit, full_page: bool | Omit = omit, - region: str | Omit = omit, + region: object | Omit = omit, use_proxy: bool | Omit = omit, # 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. diff --git a/src/steel/_files.py b/src/steel/_files.py index cc14c14..b338620 100644 --- a/src/steel/_files.py +++ b/src/steel/_files.py @@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/steel-dev/steel-python/tree/main#file-uploads" ) from None diff --git a/src/steel/resources/extensions.py b/src/steel/resources/extensions.py index 4f4bfba..6f20af7 100644 --- a/src/steel/resources/extensions.py +++ b/src/steel/resources/extensions.py @@ -2,11 +2,13 @@ from __future__ import annotations +from typing import Mapping, cast + import httpx from ..types import extension_update_params, extension_upload_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -49,7 +51,7 @@ def update( self, extension_id: str, *, - file: object | Omit = omit, + file: FileTypes | Omit = omit, url: str | Omit = omit, # 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. @@ -77,19 +79,21 @@ def update( """ if not extension_id: raise ValueError(f"Expected a non-empty value for `extension_id` but received {extension_id!r}") + body = deepcopy_minimal( + { + "file": file, + "url": url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._put( f"/v1/extensions/{extension_id}", - body=maybe_transform( - { - "file": file, - "url": url, - }, - extension_update_params.ExtensionUpdateParams, - ), + body=maybe_transform(body, extension_update_params.ExtensionUpdateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -203,7 +207,7 @@ def download( def upload( self, *, - file: object | Omit = omit, + file: FileTypes | Omit = omit, url: str | Omit = omit, # 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. @@ -229,19 +233,21 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "file": file, + "url": url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( "/v1/extensions", - body=maybe_transform( - { - "file": file, - "url": url, - }, - extension_upload_params.ExtensionUploadParams, - ), + body=maybe_transform(body, extension_upload_params.ExtensionUploadParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -273,7 +279,7 @@ async def update( self, extension_id: str, *, - file: object | Omit = omit, + file: FileTypes | Omit = omit, url: str | Omit = omit, # 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. @@ -301,19 +307,21 @@ async def update( """ if not extension_id: raise ValueError(f"Expected a non-empty value for `extension_id` but received {extension_id!r}") + body = deepcopy_minimal( + { + "file": file, + "url": url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._put( f"/v1/extensions/{extension_id}", - body=await async_maybe_transform( - { - "file": file, - "url": url, - }, - extension_update_params.ExtensionUpdateParams, - ), + body=await async_maybe_transform(body, extension_update_params.ExtensionUpdateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -427,7 +435,7 @@ async def download( async def upload( self, *, - file: object | Omit = omit, + file: FileTypes | Omit = omit, url: str | Omit = omit, # 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. @@ -453,19 +461,21 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "file": file, + "url": url, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( "/v1/extensions", - body=await async_maybe_transform( - { - "file": file, - "url": url, - }, - extension_upload_params.ExtensionUploadParams, - ), + body=await async_maybe_transform(body, extension_upload_params.ExtensionUploadParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/steel/resources/files.py b/src/steel/resources/files.py index 3bca8f2..a2e7205 100644 --- a/src/steel/resources/files.py +++ b/src/steel/resources/files.py @@ -2,11 +2,13 @@ from __future__ import annotations +from typing import Mapping, cast + import httpx from ..types import file_upload_params -from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -140,7 +142,7 @@ def download( def upload( self, *, - file: object | Omit = omit, + file: FileTypes, path: str | Omit = omit, # 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. @@ -167,19 +169,21 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "file": file, + "path": path, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( "/v1/files", - body=maybe_transform( - { - "file": file, - "path": path, - }, - file_upload_params.FileUploadParams, - ), + body=maybe_transform(body, file_upload_params.FileUploadParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -297,7 +301,7 @@ async def download( async def upload( self, *, - file: object | Omit = omit, + file: FileTypes, path: str | Omit = omit, # 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. @@ -324,19 +328,21 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "file": file, + "path": path, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( "/v1/files", - body=await async_maybe_transform( - { - "file": file, - "path": path, - }, - file_upload_params.FileUploadParams, - ), + body=await async_maybe_transform(body, file_upload_params.FileUploadParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/steel/resources/profiles.py b/src/steel/resources/profiles.py index b5337e2..3e14950 100644 --- a/src/steel/resources/profiles.py +++ b/src/steel/resources/profiles.py @@ -2,11 +2,13 @@ from __future__ import annotations +from typing import Mapping, cast + import httpx -from ..types import profile_create_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from ..types import profile_create_params, profile_update_params +from .._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given +from .._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -16,8 +18,10 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.profile_get_response import ProfileGetResponse from ..types.profile_list_response import ProfileListResponse from ..types.profile_create_response import ProfileCreateResponse +from ..types.profile_update_response import ProfileUpdateResponse __all__ = ["ProfilesResource", "AsyncProfilesResource"] @@ -45,10 +49,10 @@ def with_streaming_response(self) -> ProfilesResourceWithStreamingResponse: def create( self, *, + user_data_dir: FileTypes, dimensions: profile_create_params.Dimensions | Omit = omit, proxy_url: str | Omit = omit, user_agent: str | Omit = omit, - user_data_dir: object | Omit = omit, # 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, @@ -60,14 +64,14 @@ def create( Create a new profile Args: + user_data_dir: The user data directory associated with the profile + dimensions: The dimensions associated with the profile proxy_url: The proxy associated with the profile user_agent: The user agent associated with the profile - user_data_dir: The user data directory associated with the profile - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -76,27 +80,89 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "user_data_dir": user_data_dir, + "dimensions": dimensions, + "proxy_url": proxy_url, + "user_agent": user_agent, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["userDataDir"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( "/v1/profiles", - body=maybe_transform( - { - "dimensions": dimensions, - "proxy_url": proxy_url, - "user_agent": user_agent, - "user_data_dir": user_data_dir, - }, - profile_create_params.ProfileCreateParams, - ), + body=maybe_transform(body, profile_create_params.ProfileCreateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=ProfileCreateResponse, ) + def update( + self, + id: str, + *, + user_data_dir: FileTypes, + dimensions: profile_update_params.Dimensions | Omit = omit, + proxy_url: str | Omit = omit, + user_agent: str | Omit = omit, + # 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, + ) -> ProfileUpdateResponse: + """ + Update an existing profile + + Args: + user_data_dir: The user data directory associated with the profile + + dimensions: The dimensions associated with the profile + + proxy_url: The proxy associated with the profile + + user_agent: The user agent associated with the profile + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + body = deepcopy_minimal( + { + "user_data_dir": user_data_dir, + "dimensions": dimensions, + "proxy_url": proxy_url, + "user_agent": user_agent, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["userDataDir"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._patch( + f"/v1/profiles/{id}", + body=maybe_transform(body, profile_update_params.ProfileUpdateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProfileUpdateResponse, + ) + def list( self, *, @@ -116,6 +182,39 @@ def list( cast_to=ProfileListResponse, ) + def get( + self, + 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, + ) -> ProfileGetResponse: + """ + Retrieve a profile by ID + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/profiles/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProfileGetResponse, + ) + class AsyncProfilesResource(AsyncAPIResource): @cached_property @@ -140,10 +239,10 @@ def with_streaming_response(self) -> AsyncProfilesResourceWithStreamingResponse: async def create( self, *, + user_data_dir: FileTypes, dimensions: profile_create_params.Dimensions | Omit = omit, proxy_url: str | Omit = omit, user_agent: str | Omit = omit, - user_data_dir: object | Omit = omit, # 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, @@ -155,14 +254,14 @@ async def create( Create a new profile Args: + user_data_dir: The user data directory associated with the profile + dimensions: The dimensions associated with the profile proxy_url: The proxy associated with the profile user_agent: The user agent associated with the profile - user_data_dir: The user data directory associated with the profile - extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -171,27 +270,89 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ + body = deepcopy_minimal( + { + "user_data_dir": user_data_dir, + "dimensions": dimensions, + "proxy_url": proxy_url, + "user_agent": user_agent, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["userDataDir"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( "/v1/profiles", - body=await async_maybe_transform( - { - "dimensions": dimensions, - "proxy_url": proxy_url, - "user_agent": user_agent, - "user_data_dir": user_data_dir, - }, - profile_create_params.ProfileCreateParams, - ), + body=await async_maybe_transform(body, profile_create_params.ProfileCreateParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=ProfileCreateResponse, ) + async def update( + self, + id: str, + *, + user_data_dir: FileTypes, + dimensions: profile_update_params.Dimensions | Omit = omit, + proxy_url: str | Omit = omit, + user_agent: str | Omit = omit, + # 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, + ) -> ProfileUpdateResponse: + """ + Update an existing profile + + Args: + user_data_dir: The user data directory associated with the profile + + dimensions: The dimensions associated with the profile + + proxy_url: The proxy associated with the profile + + user_agent: The user agent associated with the profile + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + body = deepcopy_minimal( + { + "user_data_dir": user_data_dir, + "dimensions": dimensions, + "proxy_url": proxy_url, + "user_agent": user_agent, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["userDataDir"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._patch( + f"/v1/profiles/{id}", + body=await async_maybe_transform(body, profile_update_params.ProfileUpdateParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProfileUpdateResponse, + ) + async def list( self, *, @@ -211,6 +372,39 @@ async def list( cast_to=ProfileListResponse, ) + async def get( + self, + 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, + ) -> ProfileGetResponse: + """ + Retrieve a profile by ID + + 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 id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/profiles/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProfileGetResponse, + ) + class ProfilesResourceWithRawResponse: def __init__(self, profiles: ProfilesResource) -> None: @@ -219,9 +413,15 @@ def __init__(self, profiles: ProfilesResource) -> None: self.create = to_raw_response_wrapper( profiles.create, ) + self.update = to_raw_response_wrapper( + profiles.update, + ) self.list = to_raw_response_wrapper( profiles.list, ) + self.get = to_raw_response_wrapper( + profiles.get, + ) class AsyncProfilesResourceWithRawResponse: @@ -231,9 +431,15 @@ def __init__(self, profiles: AsyncProfilesResource) -> None: self.create = async_to_raw_response_wrapper( profiles.create, ) + self.update = async_to_raw_response_wrapper( + profiles.update, + ) self.list = async_to_raw_response_wrapper( profiles.list, ) + self.get = async_to_raw_response_wrapper( + profiles.get, + ) class ProfilesResourceWithStreamingResponse: @@ -243,9 +449,15 @@ def __init__(self, profiles: ProfilesResource) -> None: self.create = to_streamed_response_wrapper( profiles.create, ) + self.update = to_streamed_response_wrapper( + profiles.update, + ) self.list = to_streamed_response_wrapper( profiles.list, ) + self.get = to_streamed_response_wrapper( + profiles.get, + ) class AsyncProfilesResourceWithStreamingResponse: @@ -255,6 +467,12 @@ def __init__(self, profiles: AsyncProfilesResource) -> None: self.create = async_to_streamed_response_wrapper( profiles.create, ) + self.update = async_to_streamed_response_wrapper( + profiles.update, + ) self.list = async_to_streamed_response_wrapper( profiles.list, ) + self.get = async_to_streamed_response_wrapper( + profiles.get, + ) diff --git a/src/steel/resources/sessions/files.py b/src/steel/resources/sessions/files.py index f5d1e99..ea7b282 100644 --- a/src/steel/resources/sessions/files.py +++ b/src/steel/resources/sessions/files.py @@ -2,10 +2,12 @@ from __future__ import annotations +from typing import Mapping, cast + import httpx -from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, FileTypes, omit, not_given +from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -229,7 +231,7 @@ def upload( self, session_id: str, *, - file: object | Omit = omit, + file: FileTypes, path: str | Omit = omit, # 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. @@ -258,19 +260,21 @@ def upload( """ if not session_id: raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + body = deepcopy_minimal( + { + "file": file, + "path": path, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return self._post( f"/v1/sessions/{session_id}/files", - body=maybe_transform( - { - "file": file, - "path": path, - }, - file_upload_params.FileUploadParams, - ), + body=maybe_transform(body, file_upload_params.FileUploadParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -477,7 +481,7 @@ async def upload( self, session_id: str, *, - file: object | Omit = omit, + file: FileTypes, path: str | Omit = omit, # 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. @@ -506,19 +510,21 @@ async def upload( """ if not session_id: raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + body = deepcopy_minimal( + { + "file": file, + "path": path, + } + ) + files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. # multipart/form-data; boundary=---abc-- extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} return await self._post( f"/v1/sessions/{session_id}/files", - body=await async_maybe_transform( - { - "file": file, - "path": path, - }, - file_upload_params.FileUploadParams, - ), + body=await async_maybe_transform(body, file_upload_params.FileUploadParams), + files=files, options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), diff --git a/src/steel/resources/sessions/sessions.py b/src/steel/resources/sessions/sessions.py index de4eb2e..a02b81c 100644 --- a/src/steel/resources/sessions/sessions.py +++ b/src/steel/resources/sessions/sessions.py @@ -93,7 +93,7 @@ def create( persist_profile: bool | Omit = omit, profile_id: str | Omit = omit, proxy_url: str | Omit = omit, - region: str | Omit = omit, + region: object | Omit = omit, session_context: session_create_params.SessionContext | Omit = omit, session_id: str | Omit = omit, solve_captcha: bool | Omit = omit, @@ -161,8 +161,7 @@ def create( api_timeout: Session timeout duration in milliseconds. Default is 300000 (5 minutes). - use_proxy: Proxy configuration for the session. Can be a boolean or array of proxy - configurations + use_proxy: Simple boolean to enable/disable Steel proxies user_agent: Custom user agent string for the browser session @@ -261,7 +260,7 @@ def list( Args: cursor_id: Cursor ID for pagination - limit: Number of sessions to return. Default is 50. + limit: Number of sessions to return. Default is 50, max is 100. status: Filter sessions by current status @@ -877,7 +876,7 @@ async def create( persist_profile: bool | Omit = omit, profile_id: str | Omit = omit, proxy_url: str | Omit = omit, - region: str | Omit = omit, + region: object | Omit = omit, session_context: session_create_params.SessionContext | Omit = omit, session_id: str | Omit = omit, solve_captcha: bool | Omit = omit, @@ -945,8 +944,7 @@ async def create( api_timeout: Session timeout duration in milliseconds. Default is 300000 (5 minutes). - use_proxy: Proxy configuration for the session. Can be a boolean or array of proxy - configurations + use_proxy: Simple boolean to enable/disable Steel proxies user_agent: Custom user agent string for the browser session @@ -1045,7 +1043,7 @@ def list( Args: cursor_id: Cursor ID for pagination - limit: Number of sessions to return. Default is 50. + limit: Number of sessions to return. Default is 50, max is 100. status: Filter sessions by current status diff --git a/src/steel/types/__init__.py b/src/steel/types/__init__.py index 766e6a4..e33ce8a 100644 --- a/src/steel/types/__init__.py +++ b/src/steel/types/__init__.py @@ -14,14 +14,17 @@ from .screenshot_response import ScreenshotResponse as ScreenshotResponse from .session_list_params import SessionListParams as SessionListParams from .client_scrape_params import ClientScrapeParams as ClientScrapeParams +from .profile_get_response import ProfileGetResponse as ProfileGetResponse from .profile_create_params import ProfileCreateParams as ProfileCreateParams from .profile_list_response import ProfileListResponse as ProfileListResponse +from .profile_update_params import ProfileUpdateParams as ProfileUpdateParams from .session_create_params import SessionCreateParams as SessionCreateParams from .credential_list_params import CredentialListParams as CredentialListParams from .extension_list_response import ExtensionListResponse as ExtensionListResponse from .extension_update_params import ExtensionUpdateParams as ExtensionUpdateParams from .extension_upload_params import ExtensionUploadParams as ExtensionUploadParams from .profile_create_response import ProfileCreateResponse as ProfileCreateResponse +from .profile_update_response import ProfileUpdateResponse as ProfileUpdateResponse from .session_computer_params import SessionComputerParams as SessionComputerParams from .session_events_response import SessionEventsResponse as SessionEventsResponse from .client_screenshot_params import ClientScreenshotParams as ClientScreenshotParams diff --git a/src/steel/types/client_pdf_params.py b/src/steel/types/client_pdf_params.py index 092ca98..9ab4f0a 100644 --- a/src/steel/types/client_pdf_params.py +++ b/src/steel/types/client_pdf_params.py @@ -16,7 +16,7 @@ class ClientPdfParams(TypedDict, total=False): delay: float """Delay before generating the PDF (in milliseconds)""" - region: str + region: object """The desired region for the action to be performed in""" use_proxy: Annotated[bool, PropertyInfo(alias="useProxy")] diff --git a/src/steel/types/client_scrape_params.py b/src/steel/types/client_scrape_params.py index 9428624..c98878c 100644 --- a/src/steel/types/client_scrape_params.py +++ b/src/steel/types/client_scrape_params.py @@ -23,7 +23,7 @@ class ClientScrapeParams(TypedDict, total=False): pdf: bool """Include a PDF in the response""" - region: str + region: object """The desired region for the action to be performed in""" screenshot: bool diff --git a/src/steel/types/client_screenshot_params.py b/src/steel/types/client_screenshot_params.py index 7231a2c..865a876 100644 --- a/src/steel/types/client_screenshot_params.py +++ b/src/steel/types/client_screenshot_params.py @@ -19,7 +19,7 @@ class ClientScreenshotParams(TypedDict, total=False): full_page: Annotated[bool, PropertyInfo(alias="fullPage")] """Capture the full page screenshot. Default is `false`.""" - region: str + region: object """The desired region for the action to be performed in""" use_proxy: Annotated[bool, PropertyInfo(alias="useProxy")] diff --git a/src/steel/types/extension_update_params.py b/src/steel/types/extension_update_params.py index 3f5a9b2..cc671e0 100644 --- a/src/steel/types/extension_update_params.py +++ b/src/steel/types/extension_update_params.py @@ -4,11 +4,13 @@ from typing_extensions import TypedDict +from .._types import FileTypes + __all__ = ["ExtensionUpdateParams"] class ExtensionUpdateParams(TypedDict, total=False): - file: object + file: FileTypes """Extension .zip/.crx file""" url: str diff --git a/src/steel/types/extension_upload_params.py b/src/steel/types/extension_upload_params.py index 85c3366..ae79a9e 100644 --- a/src/steel/types/extension_upload_params.py +++ b/src/steel/types/extension_upload_params.py @@ -4,11 +4,13 @@ from typing_extensions import TypedDict +from .._types import FileTypes + __all__ = ["ExtensionUploadParams"] class ExtensionUploadParams(TypedDict, total=False): - file: object + file: FileTypes """Extension .zip/.crx file""" url: str diff --git a/src/steel/types/file_upload_params.py b/src/steel/types/file_upload_params.py index cc93f87..068bfbf 100644 --- a/src/steel/types/file_upload_params.py +++ b/src/steel/types/file_upload_params.py @@ -2,13 +2,15 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Required, TypedDict + +from .._types import FileTypes __all__ = ["FileUploadParams"] class FileUploadParams(TypedDict, total=False): - file: object + file: Required[FileTypes] """The file to upload (binary) or URL string to download from""" path: str diff --git a/src/steel/types/profile_create_params.py b/src/steel/types/profile_create_params.py index f0858a8..e45f504 100644 --- a/src/steel/types/profile_create_params.py +++ b/src/steel/types/profile_create_params.py @@ -4,12 +4,16 @@ from typing_extensions import Required, Annotated, TypedDict +from .._types import FileTypes from .._utils import PropertyInfo __all__ = ["ProfileCreateParams", "Dimensions"] class ProfileCreateParams(TypedDict, total=False): + user_data_dir: Required[Annotated[FileTypes, PropertyInfo(alias="userDataDir")]] + """The user data directory associated with the profile""" + dimensions: Dimensions """The dimensions associated with the profile""" @@ -19,9 +23,6 @@ class ProfileCreateParams(TypedDict, total=False): user_agent: Annotated[str, PropertyInfo(alias="userAgent")] """The user agent associated with the profile""" - user_data_dir: Annotated[object, PropertyInfo(alias="userDataDir")] - """The user data directory associated with the profile""" - class Dimensions(TypedDict, total=False): """The dimensions associated with the profile""" diff --git a/src/steel/types/profile_create_response.py b/src/steel/types/profile_create_response.py index 4ca0e7c..1769358 100644 --- a/src/steel/types/profile_create_response.py +++ b/src/steel/types/profile_create_response.py @@ -482,6 +482,9 @@ class ProfileCreateResponse(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") """The date and time when the profile was created""" + credentials_config: object = FieldInfo(alias="credentialsConfig") + """The credentials configuration associated with the profile""" + dimensions: Optional[Dimensions] = None """The dimensions associated with the profile""" @@ -494,17 +497,14 @@ class ProfileCreateResponse(BaseModel): source_session_id: Optional[str] = FieldInfo(alias="sourceSessionId", default=None) """The last session ID associated with the profile""" - status: Literal["UPLOADING", "READY", "FAILED"] + status: Literal["UPLOADING", "READY", "FAILED", "DELETED"] """The status of the profile""" updated_at: datetime = FieldInfo(alias="updatedAt") """The date and time when the profile was last updated""" + use_proxy_config: object = FieldInfo(alias="useProxyConfig") + """The proxy configuration associated with the profile""" + user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None) """The user agent associated with the profile""" - - credentials_config: Optional[object] = FieldInfo(alias="credentialsConfig", default=None) - """The credentials configuration associated with the profile""" - - use_proxy_config: Optional[object] = FieldInfo(alias="useProxyConfig", default=None) - """The proxy configuration associated with the profile""" diff --git a/src/steel/types/profile_get_response.py b/src/steel/types/profile_get_response.py new file mode 100644 index 0000000..d9c00a7 --- /dev/null +++ b/src/steel/types/profile_get_response.py @@ -0,0 +1,510 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import TYPE_CHECKING, Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = [ + "ProfileGetResponse", + "Dimensions", + "Fingerprint", + "FingerprintFingerprint", + "FingerprintFingerprintBattery", + "FingerprintFingerprintMultimediaDevices", + "FingerprintFingerprintMultimediaDevicesMicro", + "FingerprintFingerprintMultimediaDevicesSpeaker", + "FingerprintFingerprintMultimediaDevicesWebcam", + "FingerprintFingerprintNavigator", + "FingerprintFingerprintNavigatorExtraProperties", + "FingerprintFingerprintNavigatorUserAgentData", + "FingerprintFingerprintNavigatorUserAgentDataBrand", + "FingerprintFingerprintPluginsData", + "FingerprintFingerprintPluginsDataPlugin", + "FingerprintFingerprintPluginsDataPluginMimeType", + "FingerprintFingerprintScreen", + "FingerprintFingerprintVideoCard", + "FingerprintHeaders", +] + + +class Dimensions(BaseModel): + """The dimensions associated with the profile""" + + height: float + + width: float + + +class FingerprintFingerprintBattery(BaseModel): + charging: Optional[bool] = None + + charging_time: Optional[float] = FieldInfo(alias="chargingTime", default=None) + + discharging_time: Optional[float] = FieldInfo(alias="dischargingTime", default=None) + + level: Optional[float] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintMultimediaDevicesMicro(BaseModel): + device_id: Optional[str] = FieldInfo(alias="deviceId", default=None) + + group_id: Optional[str] = FieldInfo(alias="groupId", default=None) + + kind: Optional[str] = None + + label: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintMultimediaDevicesSpeaker(BaseModel): + device_id: Optional[str] = FieldInfo(alias="deviceId", default=None) + + group_id: Optional[str] = FieldInfo(alias="groupId", default=None) + + kind: Optional[str] = None + + label: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintMultimediaDevicesWebcam(BaseModel): + device_id: Optional[str] = FieldInfo(alias="deviceId", default=None) + + group_id: Optional[str] = FieldInfo(alias="groupId", default=None) + + kind: Optional[str] = None + + label: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintMultimediaDevices(BaseModel): + micros: List[FingerprintFingerprintMultimediaDevicesMicro] + + speakers: List[FingerprintFingerprintMultimediaDevicesSpeaker] + + webcams: List[FingerprintFingerprintMultimediaDevicesWebcam] + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintNavigatorExtraProperties(BaseModel): + global_privacy_control: Optional[bool] = FieldInfo(alias="globalPrivacyControl", default=None) + + installed_apps: List[Optional[str]] = FieldInfo(alias="installedApps") + + pdf_viewer_enabled: Optional[bool] = FieldInfo(alias="pdfViewerEnabled", default=None) + + vendor_flavors: List[Optional[str]] = FieldInfo(alias="vendorFlavors") + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintNavigatorUserAgentDataBrand(BaseModel): + brand: Optional[str] = None + + version: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintNavigatorUserAgentData(BaseModel): + brands: List[FingerprintFingerprintNavigatorUserAgentDataBrand] + + mobile: Optional[bool] = None + + platform: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintNavigator(BaseModel): + app_code_name: Optional[str] = FieldInfo(alias="appCodeName", default=None) + + app_name: Optional[str] = FieldInfo(alias="appName", default=None) + + app_version: Optional[str] = FieldInfo(alias="appVersion", default=None) + + device_memory: Optional[float] = FieldInfo(alias="deviceMemory", default=None) + + extra_properties: FingerprintFingerprintNavigatorExtraProperties = FieldInfo(alias="extraProperties") + + hardware_concurrency: Optional[float] = FieldInfo(alias="hardwareConcurrency", default=None) + + language: Optional[str] = None + + languages: List[Optional[str]] + + max_touch_points: Optional[float] = FieldInfo(alias="maxTouchPoints", default=None) + + oscpu: Optional[str] = None + + platform: Optional[str] = None + + product: Optional[str] = None + + product_sub: Optional[str] = FieldInfo(alias="productSub", default=None) + + user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None) + + user_agent_data: FingerprintFingerprintNavigatorUserAgentData = FieldInfo(alias="userAgentData") + + vendor: Optional[str] = None + + vendor_sub: Optional[str] = FieldInfo(alias="vendorSub", default=None) + + webdriver: Optional[bool] = None + + do_not_track: Optional[str] = FieldInfo(alias="doNotTrack", default=None) + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintPluginsDataPluginMimeType(BaseModel): + description: Optional[str] = None + + enabled_plugin: Optional[str] = FieldInfo(alias="enabledPlugin", default=None) + + suffixes: Optional[str] = None + + type: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintPluginsDataPlugin(BaseModel): + description: Optional[str] = None + + filename: Optional[str] = None + + mime_types: List[FingerprintFingerprintPluginsDataPluginMimeType] = FieldInfo(alias="mimeTypes") + + name: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintPluginsData(BaseModel): + mime_types: List[Optional[str]] = FieldInfo(alias="mimeTypes") + + plugins: List[FingerprintFingerprintPluginsDataPlugin] + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintScreen(BaseModel): + avail_height: Optional[float] = FieldInfo(alias="availHeight", default=None) + + avail_left: Optional[float] = FieldInfo(alias="availLeft", default=None) + + avail_top: Optional[float] = FieldInfo(alias="availTop", default=None) + + avail_width: Optional[float] = FieldInfo(alias="availWidth", default=None) + + client_height: Optional[float] = FieldInfo(alias="clientHeight", default=None) + + client_width: Optional[float] = FieldInfo(alias="clientWidth", default=None) + + color_depth: Optional[float] = FieldInfo(alias="colorDepth", default=None) + + device_pixel_ratio: Optional[float] = FieldInfo(alias="devicePixelRatio", default=None) + + has_hdr: Optional[bool] = FieldInfo(alias="hasHDR", default=None) + + height: Optional[float] = None + + inner_height: Optional[float] = FieldInfo(alias="innerHeight", default=None) + + inner_width: Optional[float] = FieldInfo(alias="innerWidth", default=None) + + outer_height: Optional[float] = FieldInfo(alias="outerHeight", default=None) + + outer_width: Optional[float] = FieldInfo(alias="outerWidth", default=None) + + page_x_offset: Optional[float] = FieldInfo(alias="pageXOffset", default=None) + + page_y_offset: Optional[float] = FieldInfo(alias="pageYOffset", default=None) + + pixel_depth: Optional[float] = FieldInfo(alias="pixelDepth", default=None) + + screen_x: Optional[float] = FieldInfo(alias="screenX", default=None) + + width: Optional[float] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintVideoCard(BaseModel): + renderer: Optional[str] = None + + vendor: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprint(BaseModel): + audio_codecs: Dict[str, Optional[str]] = FieldInfo(alias="audioCodecs") + + battery: FingerprintFingerprintBattery + + fonts: List[Optional[str]] + + mock_web_rtc: Optional[bool] = FieldInfo(alias="mockWebRTC", default=None) + + multimedia_devices: FingerprintFingerprintMultimediaDevices = FieldInfo(alias="multimediaDevices") + + navigator: FingerprintFingerprintNavigator + + plugins_data: FingerprintFingerprintPluginsData = FieldInfo(alias="pluginsData") + + screen: FingerprintFingerprintScreen + + slim: Optional[bool] = None + + video_card: FingerprintFingerprintVideoCard = FieldInfo(alias="videoCard") + + video_codecs: Dict[str, Optional[str]] = FieldInfo(alias="videoCodecs") + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintHeaders(BaseModel): + user_agent: Optional[str] = FieldInfo(alias="user-agent", default=None) + + accept: Optional[str] = None + + accept_encoding: Optional[str] = FieldInfo(alias="accept-encoding", default=None) + + accept_language: Optional[str] = FieldInfo(alias="accept-language", default=None) + + dnt: Optional[str] = None + + sec_ch_ua: Optional[str] = FieldInfo(alias="sec-ch-ua", default=None) + + sec_ch_ua_mobile: Optional[str] = FieldInfo(alias="sec-ch-ua-mobile", default=None) + + sec_ch_ua_platform: Optional[str] = FieldInfo(alias="sec-ch-ua-platform", default=None) + + sec_fetch_dest: Optional[str] = FieldInfo(alias="sec-fetch-dest", default=None) + + sec_fetch_mode: Optional[str] = FieldInfo(alias="sec-fetch-mode", default=None) + + sec_fetch_site: Optional[str] = FieldInfo(alias="sec-fetch-site", default=None) + + sec_fetch_user: Optional[str] = FieldInfo(alias="sec-fetch-user", default=None) + + upgrade_insecure_requests: Optional[str] = FieldInfo(alias="upgrade-insecure-requests", default=None) + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class Fingerprint(BaseModel): + """The fingerprint associated with the profile""" + + fingerprint: FingerprintFingerprint + + headers: FingerprintHeaders + + +class ProfileGetResponse(BaseModel): + id: str + """The unique identifier for the profile""" + + created_at: datetime = FieldInfo(alias="createdAt") + """The date and time when the profile was created""" + + credentials_config: object = FieldInfo(alias="credentialsConfig") + """The credentials configuration associated with the profile""" + + dimensions: Optional[Dimensions] = None + """The dimensions associated with the profile""" + + extension_ids: Optional[List[str]] = FieldInfo(alias="extensionIds", default=None) + """The extension IDs associated with the profile""" + + fingerprint: Optional[Fingerprint] = None + """The fingerprint associated with the profile""" + + source_session_id: Optional[str] = FieldInfo(alias="sourceSessionId", default=None) + """The last session ID associated with the profile""" + + status: Literal["UPLOADING", "READY", "FAILED", "DELETED"] + """The status of the profile""" + + updated_at: datetime = FieldInfo(alias="updatedAt") + """The date and time when the profile was last updated""" + + use_proxy_config: object = FieldInfo(alias="useProxyConfig") + """The proxy configuration associated with the profile""" + + user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None) + """The user agent associated with the profile""" diff --git a/src/steel/types/profile_list_response.py b/src/steel/types/profile_list_response.py index cecb348..325a4fb 100644 --- a/src/steel/types/profile_list_response.py +++ b/src/steel/types/profile_list_response.py @@ -483,6 +483,9 @@ class Profile(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") """The date and time when the profile was created""" + credentials_config: object = FieldInfo(alias="credentialsConfig") + """The credentials configuration associated with the profile""" + dimensions: Optional[ProfileDimensions] = None """The dimensions associated with the profile""" @@ -495,21 +498,18 @@ class Profile(BaseModel): source_session_id: Optional[str] = FieldInfo(alias="sourceSessionId", default=None) """The last session ID associated with the profile""" - status: Literal["UPLOADING", "READY", "FAILED"] + status: Literal["UPLOADING", "READY", "FAILED", "DELETED"] """The status of the profile""" updated_at: datetime = FieldInfo(alias="updatedAt") """The date and time when the profile was last updated""" + use_proxy_config: object = FieldInfo(alias="useProxyConfig") + """The proxy configuration associated with the profile""" + user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None) """The user agent associated with the profile""" - credentials_config: Optional[object] = FieldInfo(alias="credentialsConfig", default=None) - """The credentials configuration associated with the profile""" - - use_proxy_config: Optional[object] = FieldInfo(alias="useProxyConfig", default=None) - """The proxy configuration associated with the profile""" - class ProfileListResponse(BaseModel): count: float diff --git a/src/steel/types/profile_update_params.py b/src/steel/types/profile_update_params.py new file mode 100644 index 0000000..90d58b8 --- /dev/null +++ b/src/steel/types/profile_update_params.py @@ -0,0 +1,32 @@ +# 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 .._types import FileTypes +from .._utils import PropertyInfo + +__all__ = ["ProfileUpdateParams", "Dimensions"] + + +class ProfileUpdateParams(TypedDict, total=False): + user_data_dir: Required[Annotated[FileTypes, PropertyInfo(alias="userDataDir")]] + """The user data directory associated with the profile""" + + dimensions: Dimensions + """The dimensions associated with the profile""" + + proxy_url: Annotated[str, PropertyInfo(alias="proxyUrl")] + """The proxy associated with the profile""" + + user_agent: Annotated[str, PropertyInfo(alias="userAgent")] + """The user agent associated with the profile""" + + +class Dimensions(TypedDict, total=False): + """The dimensions associated with the profile""" + + height: Required[float] + + width: Required[float] diff --git a/src/steel/types/profile_update_response.py b/src/steel/types/profile_update_response.py new file mode 100644 index 0000000..c3292bb --- /dev/null +++ b/src/steel/types/profile_update_response.py @@ -0,0 +1,510 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import TYPE_CHECKING, Dict, List, Optional +from datetime import datetime +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = [ + "ProfileUpdateResponse", + "Dimensions", + "Fingerprint", + "FingerprintFingerprint", + "FingerprintFingerprintBattery", + "FingerprintFingerprintMultimediaDevices", + "FingerprintFingerprintMultimediaDevicesMicro", + "FingerprintFingerprintMultimediaDevicesSpeaker", + "FingerprintFingerprintMultimediaDevicesWebcam", + "FingerprintFingerprintNavigator", + "FingerprintFingerprintNavigatorExtraProperties", + "FingerprintFingerprintNavigatorUserAgentData", + "FingerprintFingerprintNavigatorUserAgentDataBrand", + "FingerprintFingerprintPluginsData", + "FingerprintFingerprintPluginsDataPlugin", + "FingerprintFingerprintPluginsDataPluginMimeType", + "FingerprintFingerprintScreen", + "FingerprintFingerprintVideoCard", + "FingerprintHeaders", +] + + +class Dimensions(BaseModel): + """The dimensions associated with the profile""" + + height: float + + width: float + + +class FingerprintFingerprintBattery(BaseModel): + charging: Optional[bool] = None + + charging_time: Optional[float] = FieldInfo(alias="chargingTime", default=None) + + discharging_time: Optional[float] = FieldInfo(alias="dischargingTime", default=None) + + level: Optional[float] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintMultimediaDevicesMicro(BaseModel): + device_id: Optional[str] = FieldInfo(alias="deviceId", default=None) + + group_id: Optional[str] = FieldInfo(alias="groupId", default=None) + + kind: Optional[str] = None + + label: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintMultimediaDevicesSpeaker(BaseModel): + device_id: Optional[str] = FieldInfo(alias="deviceId", default=None) + + group_id: Optional[str] = FieldInfo(alias="groupId", default=None) + + kind: Optional[str] = None + + label: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintMultimediaDevicesWebcam(BaseModel): + device_id: Optional[str] = FieldInfo(alias="deviceId", default=None) + + group_id: Optional[str] = FieldInfo(alias="groupId", default=None) + + kind: Optional[str] = None + + label: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintMultimediaDevices(BaseModel): + micros: List[FingerprintFingerprintMultimediaDevicesMicro] + + speakers: List[FingerprintFingerprintMultimediaDevicesSpeaker] + + webcams: List[FingerprintFingerprintMultimediaDevicesWebcam] + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintNavigatorExtraProperties(BaseModel): + global_privacy_control: Optional[bool] = FieldInfo(alias="globalPrivacyControl", default=None) + + installed_apps: List[Optional[str]] = FieldInfo(alias="installedApps") + + pdf_viewer_enabled: Optional[bool] = FieldInfo(alias="pdfViewerEnabled", default=None) + + vendor_flavors: List[Optional[str]] = FieldInfo(alias="vendorFlavors") + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintNavigatorUserAgentDataBrand(BaseModel): + brand: Optional[str] = None + + version: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintNavigatorUserAgentData(BaseModel): + brands: List[FingerprintFingerprintNavigatorUserAgentDataBrand] + + mobile: Optional[bool] = None + + platform: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintNavigator(BaseModel): + app_code_name: Optional[str] = FieldInfo(alias="appCodeName", default=None) + + app_name: Optional[str] = FieldInfo(alias="appName", default=None) + + app_version: Optional[str] = FieldInfo(alias="appVersion", default=None) + + device_memory: Optional[float] = FieldInfo(alias="deviceMemory", default=None) + + extra_properties: FingerprintFingerprintNavigatorExtraProperties = FieldInfo(alias="extraProperties") + + hardware_concurrency: Optional[float] = FieldInfo(alias="hardwareConcurrency", default=None) + + language: Optional[str] = None + + languages: List[Optional[str]] + + max_touch_points: Optional[float] = FieldInfo(alias="maxTouchPoints", default=None) + + oscpu: Optional[str] = None + + platform: Optional[str] = None + + product: Optional[str] = None + + product_sub: Optional[str] = FieldInfo(alias="productSub", default=None) + + user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None) + + user_agent_data: FingerprintFingerprintNavigatorUserAgentData = FieldInfo(alias="userAgentData") + + vendor: Optional[str] = None + + vendor_sub: Optional[str] = FieldInfo(alias="vendorSub", default=None) + + webdriver: Optional[bool] = None + + do_not_track: Optional[str] = FieldInfo(alias="doNotTrack", default=None) + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintPluginsDataPluginMimeType(BaseModel): + description: Optional[str] = None + + enabled_plugin: Optional[str] = FieldInfo(alias="enabledPlugin", default=None) + + suffixes: Optional[str] = None + + type: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintPluginsDataPlugin(BaseModel): + description: Optional[str] = None + + filename: Optional[str] = None + + mime_types: List[FingerprintFingerprintPluginsDataPluginMimeType] = FieldInfo(alias="mimeTypes") + + name: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintPluginsData(BaseModel): + mime_types: List[Optional[str]] = FieldInfo(alias="mimeTypes") + + plugins: List[FingerprintFingerprintPluginsDataPlugin] + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintScreen(BaseModel): + avail_height: Optional[float] = FieldInfo(alias="availHeight", default=None) + + avail_left: Optional[float] = FieldInfo(alias="availLeft", default=None) + + avail_top: Optional[float] = FieldInfo(alias="availTop", default=None) + + avail_width: Optional[float] = FieldInfo(alias="availWidth", default=None) + + client_height: Optional[float] = FieldInfo(alias="clientHeight", default=None) + + client_width: Optional[float] = FieldInfo(alias="clientWidth", default=None) + + color_depth: Optional[float] = FieldInfo(alias="colorDepth", default=None) + + device_pixel_ratio: Optional[float] = FieldInfo(alias="devicePixelRatio", default=None) + + has_hdr: Optional[bool] = FieldInfo(alias="hasHDR", default=None) + + height: Optional[float] = None + + inner_height: Optional[float] = FieldInfo(alias="innerHeight", default=None) + + inner_width: Optional[float] = FieldInfo(alias="innerWidth", default=None) + + outer_height: Optional[float] = FieldInfo(alias="outerHeight", default=None) + + outer_width: Optional[float] = FieldInfo(alias="outerWidth", default=None) + + page_x_offset: Optional[float] = FieldInfo(alias="pageXOffset", default=None) + + page_y_offset: Optional[float] = FieldInfo(alias="pageYOffset", default=None) + + pixel_depth: Optional[float] = FieldInfo(alias="pixelDepth", default=None) + + screen_x: Optional[float] = FieldInfo(alias="screenX", default=None) + + width: Optional[float] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprintVideoCard(BaseModel): + renderer: Optional[str] = None + + vendor: Optional[str] = None + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintFingerprint(BaseModel): + audio_codecs: Dict[str, Optional[str]] = FieldInfo(alias="audioCodecs") + + battery: FingerprintFingerprintBattery + + fonts: List[Optional[str]] + + mock_web_rtc: Optional[bool] = FieldInfo(alias="mockWebRTC", default=None) + + multimedia_devices: FingerprintFingerprintMultimediaDevices = FieldInfo(alias="multimediaDevices") + + navigator: FingerprintFingerprintNavigator + + plugins_data: FingerprintFingerprintPluginsData = FieldInfo(alias="pluginsData") + + screen: FingerprintFingerprintScreen + + slim: Optional[bool] = None + + video_card: FingerprintFingerprintVideoCard = FieldInfo(alias="videoCard") + + video_codecs: Dict[str, Optional[str]] = FieldInfo(alias="videoCodecs") + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class FingerprintHeaders(BaseModel): + user_agent: Optional[str] = FieldInfo(alias="user-agent", default=None) + + accept: Optional[str] = None + + accept_encoding: Optional[str] = FieldInfo(alias="accept-encoding", default=None) + + accept_language: Optional[str] = FieldInfo(alias="accept-language", default=None) + + dnt: Optional[str] = None + + sec_ch_ua: Optional[str] = FieldInfo(alias="sec-ch-ua", default=None) + + sec_ch_ua_mobile: Optional[str] = FieldInfo(alias="sec-ch-ua-mobile", default=None) + + sec_ch_ua_platform: Optional[str] = FieldInfo(alias="sec-ch-ua-platform", default=None) + + sec_fetch_dest: Optional[str] = FieldInfo(alias="sec-fetch-dest", default=None) + + sec_fetch_mode: Optional[str] = FieldInfo(alias="sec-fetch-mode", default=None) + + sec_fetch_site: Optional[str] = FieldInfo(alias="sec-fetch-site", default=None) + + sec_fetch_user: Optional[str] = FieldInfo(alias="sec-fetch-user", default=None) + + upgrade_insecure_requests: Optional[str] = FieldInfo(alias="upgrade-insecure-requests", default=None) + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] + + +class Fingerprint(BaseModel): + """The fingerprint associated with the profile""" + + fingerprint: FingerprintFingerprint + + headers: FingerprintHeaders + + +class ProfileUpdateResponse(BaseModel): + id: str + """The unique identifier for the profile""" + + created_at: datetime = FieldInfo(alias="createdAt") + """The date and time when the profile was created""" + + credentials_config: object = FieldInfo(alias="credentialsConfig") + """The credentials configuration associated with the profile""" + + dimensions: Optional[Dimensions] = None + """The dimensions associated with the profile""" + + extension_ids: Optional[List[str]] = FieldInfo(alias="extensionIds", default=None) + """The extension IDs associated with the profile""" + + fingerprint: Optional[Fingerprint] = None + """The fingerprint associated with the profile""" + + source_session_id: Optional[str] = FieldInfo(alias="sourceSessionId", default=None) + """The last session ID associated with the profile""" + + status: Literal["UPLOADING", "READY", "FAILED", "DELETED"] + """The status of the profile""" + + updated_at: datetime = FieldInfo(alias="updatedAt") + """The date and time when the profile was last updated""" + + use_proxy_config: object = FieldInfo(alias="useProxyConfig") + """The proxy configuration associated with the profile""" + + user_agent: Optional[str] = FieldInfo(alias="userAgent", default=None) + """The user agent associated with the profile""" diff --git a/src/steel/types/session_context.py b/src/steel/types/session_context.py index dc522c7..4e55302 100644 --- a/src/steel/types/session_context.py +++ b/src/steel/types/session_context.py @@ -100,11 +100,11 @@ class IndexedDBDataRecordBlobFile(BaseModel): class IndexedDBDataRecord(BaseModel): - blob_files: Optional[List[IndexedDBDataRecordBlobFile]] = FieldInfo(alias="blobFiles", default=None) + key: object - key: Optional[object] = None + value: object - value: Optional[object] = None + blob_files: Optional[List[IndexedDBDataRecordBlobFile]] = FieldInfo(alias="blobFiles", default=None) class IndexedDBData(BaseModel): diff --git a/src/steel/types/session_create_params.py b/src/steel/types/session_create_params.py index ca651c4..9774abf 100644 --- a/src/steel/types/session_create_params.py +++ b/src/steel/types/session_create_params.py @@ -96,7 +96,7 @@ class SessionCreateParams(TypedDict, total=False): proxy. Format: http(s)://username:password@hostname:port """ - region: str + region: object """The desired region for the session to be started in. Available regions are lax, ord, iad @@ -121,10 +121,7 @@ class SessionCreateParams(TypedDict, total=False): """Session timeout duration in milliseconds. Default is 300000 (5 minutes).""" use_proxy: Annotated[UseProxy, PropertyInfo(alias="useProxy")] - """Proxy configuration for the session. - - Can be a boolean or array of proxy configurations - """ + """Simple boolean to enable/disable Steel proxies""" user_agent: Annotated[str, PropertyInfo(alias="userAgent")] """Custom user agent string for the browser session""" @@ -275,11 +272,11 @@ class SessionContextIndexedDBDataRecordBlobFile(TypedDict, total=False): class SessionContextIndexedDBDataRecord(TypedDict, total=False): - blob_files: Annotated[Iterable[SessionContextIndexedDBDataRecordBlobFile], PropertyInfo(alias="blobFiles")] + key: Required[object] - key: object + value: Required[object] - value: object + blob_files: Annotated[Iterable[SessionContextIndexedDBDataRecordBlobFile], PropertyInfo(alias="blobFiles")] class SessionContextIndexedDBData(TypedDict, total=False): diff --git a/src/steel/types/session_list_params.py b/src/steel/types/session_list_params.py index bb970d6..3551725 100644 --- a/src/steel/types/session_list_params.py +++ b/src/steel/types/session_list_params.py @@ -14,7 +14,7 @@ class SessionListParams(TypedDict, total=False): """Cursor ID for pagination""" limit: int - """Number of sessions to return. Default is 50.""" + """Number of sessions to return. Default is 50, max is 100.""" status: Literal["live", "released", "failed"] """Filter sessions by current status""" diff --git a/src/steel/types/sessions/file_upload_params.py b/src/steel/types/sessions/file_upload_params.py index cc93f87..c80634f 100644 --- a/src/steel/types/sessions/file_upload_params.py +++ b/src/steel/types/sessions/file_upload_params.py @@ -2,13 +2,15 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Required, TypedDict + +from ..._types import FileTypes __all__ = ["FileUploadParams"] class FileUploadParams(TypedDict, total=False): - file: object + file: Required[FileTypes] """The file to upload (binary) or URL string to download from""" path: str diff --git a/src/steel/types/sessionslist.py b/src/steel/types/sessionslist.py index 7a451a8..8c836a2 100644 --- a/src/steel/types/sessionslist.py +++ b/src/steel/types/sessionslist.py @@ -165,5 +165,11 @@ class Session(BaseModel): class Sessionslist(BaseModel): """Response containing a list of browser sessions with pagination details.""" + next_cursor: Optional[str] = FieldInfo(alias="nextCursor", default=None) + """Cursor for the next page of results. Null if no more pages.""" + sessions: List[Session] """List of browser sessions""" + + total_count: int = FieldInfo(alias="totalCount") + """Total number of sessions matching the query""" diff --git a/tests/api_resources/sessions/test_files.py b/tests/api_resources/sessions/test_files.py index 66f2299..be2dbb0 100644 --- a/tests/api_resources/sessions/test_files.py +++ b/tests/api_resources/sessions/test_files.py @@ -263,6 +263,7 @@ def test_path_params_download_archive(self, client: Steel) -> None: def test_method_upload(self, client: Steel) -> None: file = client.sessions.files.upload( session_id="sessionId", + file=b"raw file contents", ) assert_matches_type(File, file, path=["response"]) @@ -270,7 +271,7 @@ def test_method_upload(self, client: Steel) -> None: def test_method_upload_with_all_params(self, client: Steel) -> None: file = client.sessions.files.upload( session_id="sessionId", - file={}, + file=b"raw file contents", path="path", ) assert_matches_type(File, file, path=["response"]) @@ -279,6 +280,7 @@ def test_method_upload_with_all_params(self, client: Steel) -> None: def test_raw_response_upload(self, client: Steel) -> None: response = client.sessions.files.with_raw_response.upload( session_id="sessionId", + file=b"raw file contents", ) assert response.is_closed is True @@ -290,6 +292,7 @@ def test_raw_response_upload(self, client: Steel) -> None: def test_streaming_response_upload(self, client: Steel) -> None: with client.sessions.files.with_streaming_response.upload( session_id="sessionId", + file=b"raw file contents", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -304,6 +307,7 @@ def test_path_params_upload(self, client: Steel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `session_id` but received ''"): client.sessions.files.with_raw_response.upload( session_id="", + file=b"raw file contents", ) @@ -550,6 +554,7 @@ async def test_path_params_download_archive(self, async_client: AsyncSteel) -> N async def test_method_upload(self, async_client: AsyncSteel) -> None: file = await async_client.sessions.files.upload( session_id="sessionId", + file=b"raw file contents", ) assert_matches_type(File, file, path=["response"]) @@ -557,7 +562,7 @@ async def test_method_upload(self, async_client: AsyncSteel) -> None: async def test_method_upload_with_all_params(self, async_client: AsyncSteel) -> None: file = await async_client.sessions.files.upload( session_id="sessionId", - file={}, + file=b"raw file contents", path="path", ) assert_matches_type(File, file, path=["response"]) @@ -566,6 +571,7 @@ async def test_method_upload_with_all_params(self, async_client: AsyncSteel) -> async def test_raw_response_upload(self, async_client: AsyncSteel) -> None: response = await async_client.sessions.files.with_raw_response.upload( session_id="sessionId", + file=b"raw file contents", ) assert response.is_closed is True @@ -577,6 +583,7 @@ async def test_raw_response_upload(self, async_client: AsyncSteel) -> None: async def test_streaming_response_upload(self, async_client: AsyncSteel) -> None: async with async_client.sessions.files.with_streaming_response.upload( session_id="sessionId", + file=b"raw file contents", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -591,4 +598,5 @@ async def test_path_params_upload(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.files.with_raw_response.upload( session_id="", + file=b"raw file contents", ) diff --git a/tests/api_resources/test_client.py b/tests/api_resources/test_client.py index 8d6214b..a4e8eca 100644 --- a/tests/api_resources/test_client.py +++ b/tests/api_resources/test_client.py @@ -33,7 +33,7 @@ def test_method_pdf_with_all_params(self, client: Steel) -> None: client_ = client.pdf( url="https://example.com", delay=0, - region="region", + region={}, use_proxy=True, ) assert_matches_type(PdfResponse, client_, path=["response"]) @@ -76,7 +76,7 @@ def test_method_scrape_with_all_params(self, client: Steel) -> None: delay=0, format=["html"], pdf=True, - region="region", + region={}, screenshot=True, use_proxy=True, ) @@ -119,7 +119,7 @@ def test_method_screenshot_with_all_params(self, client: Steel) -> None: url="https://example.com", delay=0, full_page=True, - region="region", + region={}, use_proxy=True, ) assert_matches_type(ScreenshotResponse, client_, path=["response"]) @@ -166,7 +166,7 @@ async def test_method_pdf_with_all_params(self, async_client: AsyncSteel) -> Non client = await async_client.pdf( url="https://example.com", delay=0, - region="region", + region={}, use_proxy=True, ) assert_matches_type(PdfResponse, client, path=["response"]) @@ -209,7 +209,7 @@ async def test_method_scrape_with_all_params(self, async_client: AsyncSteel) -> delay=0, format=["html"], pdf=True, - region="region", + region={}, screenshot=True, use_proxy=True, ) @@ -252,7 +252,7 @@ async def test_method_screenshot_with_all_params(self, async_client: AsyncSteel) url="https://example.com", delay=0, full_page=True, - region="region", + region={}, use_proxy=True, ) assert_matches_type(ScreenshotResponse, client, path=["response"]) diff --git a/tests/api_resources/test_extensions.py b/tests/api_resources/test_extensions.py index 6b4b14b..b996a46 100644 --- a/tests/api_resources/test_extensions.py +++ b/tests/api_resources/test_extensions.py @@ -34,7 +34,7 @@ def test_method_update(self, client: Steel) -> None: def test_method_update_with_all_params(self, client: Steel) -> None: extension = client.extensions.update( extension_id="extensionId", - file={}, + file=b"raw file contents", url="https://example.com", ) assert_matches_type(ExtensionUpdateResponse, extension, path=["response"]) @@ -204,7 +204,7 @@ def test_method_upload(self, client: Steel) -> None: @parametrize def test_method_upload_with_all_params(self, client: Steel) -> None: extension = client.extensions.upload( - file={}, + file=b"raw file contents", url="https://example.com", ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) @@ -246,7 +246,7 @@ async def test_method_update(self, async_client: AsyncSteel) -> None: async def test_method_update_with_all_params(self, async_client: AsyncSteel) -> None: extension = await async_client.extensions.update( extension_id="extensionId", - file={}, + file=b"raw file contents", url="https://example.com", ) assert_matches_type(ExtensionUpdateResponse, extension, path=["response"]) @@ -416,7 +416,7 @@ async def test_method_upload(self, async_client: AsyncSteel) -> None: @parametrize async def test_method_upload_with_all_params(self, async_client: AsyncSteel) -> None: extension = await async_client.extensions.upload( - file={}, + file=b"raw file contents", url="https://example.com", ) assert_matches_type(ExtensionUploadResponse, extension, path=["response"]) diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index a057032..5ca440d 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -140,20 +140,24 @@ def test_path_params_download(self, client: Steel) -> None: @parametrize def test_method_upload(self, client: Steel) -> None: - file = client.files.upload() + file = client.files.upload( + file=b"raw file contents", + ) assert_matches_type(File, file, path=["response"]) @parametrize def test_method_upload_with_all_params(self, client: Steel) -> None: file = client.files.upload( - file={}, + file=b"raw file contents", path="path", ) assert_matches_type(File, file, path=["response"]) @parametrize def test_raw_response_upload(self, client: Steel) -> None: - response = client.files.with_raw_response.upload() + response = client.files.with_raw_response.upload( + file=b"raw file contents", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -162,7 +166,9 @@ def test_raw_response_upload(self, client: Steel) -> None: @parametrize def test_streaming_response_upload(self, client: Steel) -> None: - with client.files.with_streaming_response.upload() as response: + with client.files.with_streaming_response.upload( + file=b"raw file contents", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -292,20 +298,24 @@ async def test_path_params_download(self, async_client: AsyncSteel) -> None: @parametrize async def test_method_upload(self, async_client: AsyncSteel) -> None: - file = await async_client.files.upload() + file = await async_client.files.upload( + file=b"raw file contents", + ) assert_matches_type(File, file, path=["response"]) @parametrize async def test_method_upload_with_all_params(self, async_client: AsyncSteel) -> None: file = await async_client.files.upload( - file={}, + file=b"raw file contents", path="path", ) assert_matches_type(File, file, path=["response"]) @parametrize async def test_raw_response_upload(self, async_client: AsyncSteel) -> None: - response = await async_client.files.with_raw_response.upload() + response = await async_client.files.with_raw_response.upload( + file=b"raw file contents", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -314,7 +324,9 @@ async def test_raw_response_upload(self, async_client: AsyncSteel) -> None: @parametrize async def test_streaming_response_upload(self, async_client: AsyncSteel) -> None: - async with async_client.files.with_streaming_response.upload() as response: + async with async_client.files.with_streaming_response.upload( + file=b"raw file contents", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_profiles.py b/tests/api_resources/test_profiles.py index ef2fb8b..24a3cd0 100644 --- a/tests/api_resources/test_profiles.py +++ b/tests/api_resources/test_profiles.py @@ -8,7 +8,12 @@ import pytest from steel import Steel, AsyncSteel -from steel.types import ProfileListResponse, ProfileCreateResponse +from steel.types import ( + ProfileGetResponse, + ProfileListResponse, + ProfileCreateResponse, + ProfileUpdateResponse, +) from tests.utils import assert_matches_type base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -19,25 +24,29 @@ class TestProfiles: @parametrize def test_method_create(self, client: Steel) -> None: - profile = client.profiles.create() + profile = client.profiles.create( + user_data_dir=b"raw file contents", + ) assert_matches_type(ProfileCreateResponse, profile, path=["response"]) @parametrize def test_method_create_with_all_params(self, client: Steel) -> None: profile = client.profiles.create( + user_data_dir=b"raw file contents", dimensions={ "height": 0, "width": 0, }, proxy_url="https://example.com", user_agent="userAgent", - user_data_dir={}, ) assert_matches_type(ProfileCreateResponse, profile, path=["response"]) @parametrize def test_raw_response_create(self, client: Steel) -> None: - response = client.profiles.with_raw_response.create() + response = client.profiles.with_raw_response.create( + user_data_dir=b"raw file contents", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -46,7 +55,9 @@ def test_raw_response_create(self, client: Steel) -> None: @parametrize def test_streaming_response_create(self, client: Steel) -> None: - with client.profiles.with_streaming_response.create() as response: + with client.profiles.with_streaming_response.create( + user_data_dir=b"raw file contents", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -55,6 +66,62 @@ def test_streaming_response_create(self, client: Steel) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_method_update(self, client: Steel) -> None: + profile = client.profiles.update( + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + user_data_dir=b"raw file contents", + ) + assert_matches_type(ProfileUpdateResponse, profile, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Steel) -> None: + profile = client.profiles.update( + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + user_data_dir=b"raw file contents", + dimensions={ + "height": 0, + "width": 0, + }, + proxy_url="https://example.com", + user_agent="userAgent", + ) + assert_matches_type(ProfileUpdateResponse, profile, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Steel) -> None: + response = client.profiles.with_raw_response.update( + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + user_data_dir=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = response.parse() + assert_matches_type(ProfileUpdateResponse, profile, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Steel) -> None: + with client.profiles.with_streaming_response.update( + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + user_data_dir=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = response.parse() + assert_matches_type(ProfileUpdateResponse, profile, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Steel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.profiles.with_raw_response.update( + id="", + user_data_dir=b"raw file contents", + ) + @parametrize def test_method_list(self, client: Steel) -> None: profile = client.profiles.list() @@ -80,6 +147,44 @@ def test_streaming_response_list(self, client: Steel) -> None: assert cast(Any, response.is_closed) is True + @parametrize + def test_method_get(self, client: Steel) -> None: + profile = client.profiles.get( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(ProfileGetResponse, profile, path=["response"]) + + @parametrize + def test_raw_response_get(self, client: Steel) -> None: + response = client.profiles.with_raw_response.get( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = response.parse() + assert_matches_type(ProfileGetResponse, profile, path=["response"]) + + @parametrize + def test_streaming_response_get(self, client: Steel) -> None: + with client.profiles.with_streaming_response.get( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = response.parse() + assert_matches_type(ProfileGetResponse, profile, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_get(self, client: Steel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.profiles.with_raw_response.get( + "", + ) + class TestAsyncProfiles: parametrize = pytest.mark.parametrize( @@ -88,25 +193,29 @@ class TestAsyncProfiles: @parametrize async def test_method_create(self, async_client: AsyncSteel) -> None: - profile = await async_client.profiles.create() + profile = await async_client.profiles.create( + user_data_dir=b"raw file contents", + ) assert_matches_type(ProfileCreateResponse, profile, path=["response"]) @parametrize async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> None: profile = await async_client.profiles.create( + user_data_dir=b"raw file contents", dimensions={ "height": 0, "width": 0, }, proxy_url="https://example.com", user_agent="userAgent", - user_data_dir={}, ) assert_matches_type(ProfileCreateResponse, profile, path=["response"]) @parametrize async def test_raw_response_create(self, async_client: AsyncSteel) -> None: - response = await async_client.profiles.with_raw_response.create() + response = await async_client.profiles.with_raw_response.create( + user_data_dir=b"raw file contents", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -115,7 +224,9 @@ async def test_raw_response_create(self, async_client: AsyncSteel) -> None: @parametrize async def test_streaming_response_create(self, async_client: AsyncSteel) -> None: - async with async_client.profiles.with_streaming_response.create() as response: + async with async_client.profiles.with_streaming_response.create( + user_data_dir=b"raw file contents", + ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -124,6 +235,62 @@ async def test_streaming_response_create(self, async_client: AsyncSteel) -> None assert cast(Any, response.is_closed) is True + @parametrize + async def test_method_update(self, async_client: AsyncSteel) -> None: + profile = await async_client.profiles.update( + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + user_data_dir=b"raw file contents", + ) + assert_matches_type(ProfileUpdateResponse, profile, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncSteel) -> None: + profile = await async_client.profiles.update( + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + user_data_dir=b"raw file contents", + dimensions={ + "height": 0, + "width": 0, + }, + proxy_url="https://example.com", + user_agent="userAgent", + ) + assert_matches_type(ProfileUpdateResponse, profile, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncSteel) -> None: + response = await async_client.profiles.with_raw_response.update( + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + user_data_dir=b"raw file contents", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = await response.parse() + assert_matches_type(ProfileUpdateResponse, profile, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncSteel) -> None: + async with async_client.profiles.with_streaming_response.update( + id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + user_data_dir=b"raw file contents", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = await response.parse() + assert_matches_type(ProfileUpdateResponse, profile, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncSteel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.profiles.with_raw_response.update( + id="", + user_data_dir=b"raw file contents", + ) + @parametrize async def test_method_list(self, async_client: AsyncSteel) -> None: profile = await async_client.profiles.list() @@ -148,3 +315,41 @@ async def test_streaming_response_list(self, async_client: AsyncSteel) -> None: assert_matches_type(ProfileListResponse, profile, path=["response"]) assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_get(self, async_client: AsyncSteel) -> None: + profile = await async_client.profiles.get( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(ProfileGetResponse, profile, path=["response"]) + + @parametrize + async def test_raw_response_get(self, async_client: AsyncSteel) -> None: + response = await async_client.profiles.with_raw_response.get( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + profile = await response.parse() + assert_matches_type(ProfileGetResponse, profile, path=["response"]) + + @parametrize + async def test_streaming_response_get(self, async_client: AsyncSteel) -> None: + async with async_client.profiles.with_streaming_response.get( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + profile = await response.parse() + assert_matches_type(ProfileGetResponse, profile, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_get(self, async_client: AsyncSteel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.profiles.with_raw_response.get( + "", + ) diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 8acdf62..4284a83 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -37,7 +37,7 @@ def test_method_create(self, client: Steel) -> None: def test_method_create_with_all_params(self, client: Steel) -> None: session = client.sessions.create( block_ads=True, - concurrency=0, + concurrency=-9007199254740991, credentials={ "auto_submit": True, "blur_fields": True, @@ -49,8 +49,8 @@ def test_method_create_with_all_params(self, client: Steel) -> None: }, device_config={"device": "desktop"}, dimensions={ - "height": 0, - "width": 0, + "height": -9007199254740991, + "width": -9007199254740991, }, extension_ids=["string"], headless=True, @@ -60,7 +60,7 @@ def test_method_create_with_all_params(self, client: Steel) -> None: persist_profile=True, profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", proxy_url="https://example.com", - region="region", + region={}, session_context={ "cookies": [ { @@ -95,6 +95,8 @@ def test_method_create_with_all_params(self, client: Steel) -> None: "name": "name", "records": [ { + "key": {}, + "value": {}, "blob_files": [ { "blob_number": 0, @@ -105,8 +107,6 @@ def test_method_create_with_all_params(self, client: Steel) -> None: "path": "path", } ], - "key": {}, - "value": {}, } ], } @@ -124,7 +124,7 @@ def test_method_create_with_all_params(self, client: Steel) -> None: "humanize_interactions": True, "skip_fingerprint_injection": True, }, - api_timeout=0, + api_timeout=-9007199254740991, use_proxy=True, user_agent="userAgent", ) @@ -197,7 +197,7 @@ def test_method_list(self, client: Steel) -> None: def test_method_list_with_all_params(self, client: Steel) -> None: session = client.sessions.list( cursor_id="cursorId", - limit=0, + limit=1, status="live", ) assert_matches_type(SyncSessionsCursor[SessionslistSession], session, path=["response"]) @@ -897,7 +897,7 @@ async def test_method_create(self, async_client: AsyncSteel) -> None: async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> None: session = await async_client.sessions.create( block_ads=True, - concurrency=0, + concurrency=-9007199254740991, credentials={ "auto_submit": True, "blur_fields": True, @@ -909,8 +909,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> }, device_config={"device": "desktop"}, dimensions={ - "height": 0, - "width": 0, + "height": -9007199254740991, + "width": -9007199254740991, }, extension_ids=["string"], headless=True, @@ -920,7 +920,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> persist_profile=True, profile_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", proxy_url="https://example.com", - region="region", + region={}, session_context={ "cookies": [ { @@ -955,6 +955,8 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> "name": "name", "records": [ { + "key": {}, + "value": {}, "blob_files": [ { "blob_number": 0, @@ -965,8 +967,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> "path": "path", } ], - "key": {}, - "value": {}, } ], } @@ -984,7 +984,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> "humanize_interactions": True, "skip_fingerprint_injection": True, }, - api_timeout=0, + api_timeout=-9007199254740991, use_proxy=True, user_agent="userAgent", ) @@ -1057,7 +1057,7 @@ async def test_method_list(self, async_client: AsyncSteel) -> None: async def test_method_list_with_all_params(self, async_client: AsyncSteel) -> None: session = await async_client.sessions.list( cursor_id="cursorId", - limit=0, + limit=1, status="live", ) assert_matches_type(AsyncSessionsCursor[SessionslistSession], session, path=["response"]) From 173f4bccb8cec99bd0cbe652203a85d200a98520 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:42:49 +0000 Subject: [PATCH 10/15] feat(client): add support for binary request streaming --- src/steel/_base_client.py | 145 +++++++++++++++++++++++++++--- src/steel/_models.py | 17 +++- src/steel/_types.py | 9 ++ tests/test_client.py | 185 +++++++++++++++++++++++++++++++++++++- 4 files changed, 342 insertions(+), 14 deletions(-) diff --git a/src/steel/_base_client.py b/src/steel/_base_client.py index 941f69d..23ac306 100644 --- a/src/steel/_base_client.py +++ b/src/steel/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/steel/_models.py b/src/steel/_models.py index ca9500b..29070e0 100644 --- a/src/steel/_models.py +++ b/src/steel/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/steel/_types.py b/src/steel/_types.py index 331c956..fabd758 100644 --- a/src/steel/_types.py +++ b/src/steel/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index e916c84..fe612e5 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -49,6 +51,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: Steel | AsyncSteel) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -465,6 +518,69 @@ def test_multipart_repeating_array(self, client: Steel) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Steel) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with Steel( + base_url=base_url, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Steel) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: Steel) -> None: class Model1(BaseModel): @@ -1243,6 +1359,71 @@ def test_multipart_repeating_array(self, async_client: AsyncSteel) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncSteel) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncSteel( + base_url=base_url, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncSteel + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncSteel) -> None: class Model1(BaseModel): From f226fef740814026024c09636063a76c6e702e0c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 07:35:56 +0000 Subject: [PATCH 11/15] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8814d63..0fc2042 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/steel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/steel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/steel-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 4e01887..a90ec94 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index c52ccfa..386e8eb 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'steel-dev/steel-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | From a950e2afb5d34aace130b42e6118ccb255639b82 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 06:34:35 +0000 Subject: [PATCH 12/15] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0fc2042..8ba6b37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/steel-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From 9c3789211e990081c0d97fd4684d480671942503 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 06:17:43 +0000 Subject: [PATCH 13/15] feat(client): add custom JSON encoder for extended type support --- src/steel/_base_client.py | 7 +- src/steel/_compat.py | 6 +- src/steel/_utils/_json.py | 35 ++++++++++ tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/steel/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/steel/_base_client.py b/src/steel/_base_client.py index 23ac306..278ca50 100644 --- a/src/steel/_base_client.py +++ b/src/steel/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/steel/_compat.py b/src/steel/_compat.py index bdef67f..786ff42 100644 --- a/src/steel/_compat.py +++ b/src/steel/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/steel/_utils/_json.py b/src/steel/_utils/_json.py new file mode 100644 index 0000000..6058421 --- /dev/null +++ b/src/steel/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 0000000..7b2931e --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from steel import _compat +from steel._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 6efa7c68d1783cf007c2854e095aaaa29cf70bde Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:09:35 +0000 Subject: [PATCH 14/15] feat(api): api update --- .stats.yml | 8 +- api.md | 7 +- src/steel/resources/sessions/captchas.py | 121 +++++++++++++++++- src/steel/types/profile_create_response.py | 2 +- src/steel/types/profile_get_response.py | 2 +- src/steel/types/profile_list_response.py | 2 +- src/steel/types/profile_update_response.py | 2 +- src/steel/types/session.py | 6 + src/steel/types/session_create_params.py | 6 + src/steel/types/sessions/__init__.py | 2 + .../types/sessions/captcha_solve_params.py | 20 +++ .../types/sessions/captcha_solve_response.py | 15 +++ src/steel/types/sessionslist.py | 6 + tests/api_resources/sessions/test_captchas.py | 102 ++++++++++++++- tests/api_resources/test_sessions.py | 2 + 15 files changed, 292 insertions(+), 11 deletions(-) create mode 100644 src/steel/types/sessions/captcha_solve_params.py create mode 100644 src/steel/types/sessions/captcha_solve_response.py diff --git a/.stats.yml b/.stats.yml index d133400..416f1d7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 38 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-97dcad9b050e2818332d548e93c2882fa5482df4dca384ce7f8d3cabdfd92b69.yml -openapi_spec_hash: 8d3fa51c0740046e4caf2a299ba9d89e -config_hash: 03d8eae69b6431c0ae0566dddaa9b0ea +configured_endpoints: 39 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-45efcdf3e5ccffb6e94a86be505b24b7b4ff05d8f1a2978c2a281729af68cb82.yml +openapi_spec_hash: 9a7724672b05d44888d67b6ed0ffc7ca +config_hash: dce4dea59023b0a00890fa654fbfffb4 diff --git a/api.md b/api.md index 04ba9bb..49ae21f 100644 --- a/api.md +++ b/api.md @@ -92,11 +92,16 @@ Methods: Types: ```python -from steel.types.sessions import CaptchaSolveImageResponse, CaptchaStatusResponse +from steel.types.sessions import ( + CaptchaSolveResponse, + CaptchaSolveImageResponse, + CaptchaStatusResponse, +) ``` Methods: +- client.sessions.captchas.solve(session_id, \*\*params) -> CaptchaSolveResponse - client.sessions.captchas.solve_image(session_id, \*\*params) -> CaptchaSolveImageResponse - client.sessions.captchas.status(session_id) -> CaptchaStatusResponse diff --git a/src/steel/resources/sessions/captchas.py b/src/steel/resources/sessions/captchas.py index dbdc591..638dc05 100644 --- a/src/steel/resources/sessions/captchas.py +++ b/src/steel/resources/sessions/captchas.py @@ -15,7 +15,8 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.sessions import captcha_solve_image_params +from ...types.sessions import captcha_solve_params, captcha_solve_image_params +from ...types.sessions.captcha_solve_response import CaptchaSolveResponse from ...types.sessions.captcha_status_response import CaptchaStatusResponse from ...types.sessions.captcha_solve_image_response import CaptchaSolveImageResponse @@ -42,6 +43,59 @@ def with_streaming_response(self) -> CaptchasResourceWithStreamingResponse: """ return CaptchasResourceWithStreamingResponse(self) + def solve( + self, + session_id: str, + *, + page_id: str | Omit = omit, + task_id: str | Omit = omit, + url: str | Omit = omit, + # 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, + ) -> CaptchaSolveResponse: + """Solves captcha(s) for the session. + + If pageId, url, or taskId is provided, solves + that specific captcha. If no parameters are provided, solves all detected + captchas. Use this when autoCaptchaSolving is disabled in stealthConfig. + + Args: + page_id: The page ID where the captcha is located + + task_id: The task ID of the specific captcha to solve + + url: The 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", + body=maybe_transform( + { + "page_id": page_id, + "task_id": task_id, + "url": url, + }, + captcha_solve_params.CaptchaSolveParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CaptchaSolveResponse, + ) + def solve_image( self, session_id: str, @@ -146,6 +200,59 @@ def with_streaming_response(self) -> AsyncCaptchasResourceWithStreamingResponse: """ return AsyncCaptchasResourceWithStreamingResponse(self) + async def solve( + self, + session_id: str, + *, + page_id: str | Omit = omit, + task_id: str | Omit = omit, + url: str | Omit = omit, + # 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, + ) -> CaptchaSolveResponse: + """Solves captcha(s) for the session. + + If pageId, url, or taskId is provided, solves + that specific captcha. If no parameters are provided, solves all detected + captchas. Use this when autoCaptchaSolving is disabled in stealthConfig. + + Args: + page_id: The page ID where the captcha is located + + task_id: The task ID of the specific captcha to solve + + url: The 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", + body=await async_maybe_transform( + { + "page_id": page_id, + "task_id": task_id, + "url": url, + }, + captcha_solve_params.CaptchaSolveParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=CaptchaSolveResponse, + ) + async def solve_image( self, session_id: str, @@ -234,6 +341,9 @@ class CaptchasResourceWithRawResponse: def __init__(self, captchas: CaptchasResource) -> None: self._captchas = captchas + self.solve = to_raw_response_wrapper( + captchas.solve, + ) self.solve_image = to_raw_response_wrapper( captchas.solve_image, ) @@ -246,6 +356,9 @@ class AsyncCaptchasResourceWithRawResponse: def __init__(self, captchas: AsyncCaptchasResource) -> None: self._captchas = captchas + self.solve = async_to_raw_response_wrapper( + captchas.solve, + ) self.solve_image = async_to_raw_response_wrapper( captchas.solve_image, ) @@ -258,6 +371,9 @@ class CaptchasResourceWithStreamingResponse: def __init__(self, captchas: CaptchasResource) -> None: self._captchas = captchas + self.solve = to_streamed_response_wrapper( + captchas.solve, + ) self.solve_image = to_streamed_response_wrapper( captchas.solve_image, ) @@ -270,6 +386,9 @@ class AsyncCaptchasResourceWithStreamingResponse: def __init__(self, captchas: AsyncCaptchasResource) -> None: self._captchas = captchas + self.solve = async_to_streamed_response_wrapper( + captchas.solve, + ) self.solve_image = async_to_streamed_response_wrapper( captchas.solve_image, ) diff --git a/src/steel/types/profile_create_response.py b/src/steel/types/profile_create_response.py index 1769358..f2b58c6 100644 --- a/src/steel/types/profile_create_response.py +++ b/src/steel/types/profile_create_response.py @@ -497,7 +497,7 @@ class ProfileCreateResponse(BaseModel): source_session_id: Optional[str] = FieldInfo(alias="sourceSessionId", default=None) """The last session ID associated with the profile""" - status: Literal["UPLOADING", "READY", "FAILED", "DELETED"] + status: Literal["UPLOADING", "READY", "FAILED"] """The status of the profile""" updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/steel/types/profile_get_response.py b/src/steel/types/profile_get_response.py index d9c00a7..f8cd486 100644 --- a/src/steel/types/profile_get_response.py +++ b/src/steel/types/profile_get_response.py @@ -497,7 +497,7 @@ class ProfileGetResponse(BaseModel): source_session_id: Optional[str] = FieldInfo(alias="sourceSessionId", default=None) """The last session ID associated with the profile""" - status: Literal["UPLOADING", "READY", "FAILED", "DELETED"] + status: Literal["UPLOADING", "READY", "FAILED"] """The status of the profile""" updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/steel/types/profile_list_response.py b/src/steel/types/profile_list_response.py index 325a4fb..1d02923 100644 --- a/src/steel/types/profile_list_response.py +++ b/src/steel/types/profile_list_response.py @@ -498,7 +498,7 @@ class Profile(BaseModel): source_session_id: Optional[str] = FieldInfo(alias="sourceSessionId", default=None) """The last session ID associated with the profile""" - status: Literal["UPLOADING", "READY", "FAILED", "DELETED"] + status: Literal["UPLOADING", "READY", "FAILED"] """The status of the profile""" updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/steel/types/profile_update_response.py b/src/steel/types/profile_update_response.py index c3292bb..909f8f8 100644 --- a/src/steel/types/profile_update_response.py +++ b/src/steel/types/profile_update_response.py @@ -497,7 +497,7 @@ class ProfileUpdateResponse(BaseModel): source_session_id: Optional[str] = FieldInfo(alias="sourceSessionId", default=None) """The last session ID associated with the profile""" - status: Literal["UPLOADING", "READY", "FAILED", "DELETED"] + status: Literal["UPLOADING", "READY", "FAILED"] """The status of the profile""" updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/steel/types/session.py b/src/steel/types/session.py index 5a13073..0aaf198 100644 --- a/src/steel/types/session.py +++ b/src/steel/types/session.py @@ -63,6 +63,12 @@ class DeviceConfig(BaseModel): class StealthConfig(BaseModel): """Stealth configuration for the session""" + auto_captcha_solving: Optional[bool] = FieldInfo(alias="autoCaptchaSolving", default=None) + """When true, captchas will be automatically solved when detected. + + When false, use the solve endpoints to manually initiate solving. + """ + humanize_interactions: Optional[bool] = FieldInfo(alias="humanizeInteractions", default=None) """ This flag will make the browser act more human-like by moving the mouse in a diff --git a/src/steel/types/session_create_params.py b/src/steel/types/session_create_params.py index 9774abf..b98d7f3 100644 --- a/src/steel/types/session_create_params.py +++ b/src/steel/types/session_create_params.py @@ -317,6 +317,12 @@ class SessionContext(TypedDict, total=False): class StealthConfig(TypedDict, total=False): """Stealth configuration for the session""" + auto_captcha_solving: Annotated[bool, PropertyInfo(alias="autoCaptchaSolving")] + """When true, captchas will be automatically solved when detected. + + When false, use the solve endpoints to manually initiate solving. + """ + humanize_interactions: Annotated[bool, PropertyInfo(alias="humanizeInteractions")] """ This flag will make the browser act more human-like by moving the mouse in a diff --git a/src/steel/types/sessions/__init__.py b/src/steel/types/sessions/__init__.py index fddfdee..966a925 100644 --- a/src/steel/types/sessions/__init__.py +++ b/src/steel/types/sessions/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations from .file_upload_params import FileUploadParams as FileUploadParams +from .captcha_solve_params import CaptchaSolveParams as CaptchaSolveParams +from .captcha_solve_response import CaptchaSolveResponse as CaptchaSolveResponse 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_params.py b/src/steel/types/sessions/captcha_solve_params.py new file mode 100644 index 0000000..49f3f39 --- /dev/null +++ b/src/steel/types/sessions/captcha_solve_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 Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["CaptchaSolveParams"] + + +class CaptchaSolveParams(TypedDict, total=False): + page_id: Annotated[str, PropertyInfo(alias="pageId")] + """The page ID where the captcha is located""" + + task_id: Annotated[str, PropertyInfo(alias="taskId")] + """The task ID of the specific captcha to solve""" + + url: str + """The URL where the captcha is located""" diff --git a/src/steel/types/sessions/captcha_solve_response.py b/src/steel/types/sessions/captcha_solve_response.py new file mode 100644 index 0000000..caf0213 --- /dev/null +++ b/src/steel/types/sessions/captcha_solve_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["CaptchaSolveResponse"] + + +class CaptchaSolveResponse(BaseModel): + success: bool + """Whether the action was successful""" + + message: Optional[str] = None + """Response message""" diff --git a/src/steel/types/sessionslist.py b/src/steel/types/sessionslist.py index 8c836a2..05604e7 100644 --- a/src/steel/types/sessionslist.py +++ b/src/steel/types/sessionslist.py @@ -71,6 +71,12 @@ class SessionDeviceConfig(BaseModel): class SessionStealthConfig(BaseModel): """Stealth configuration for the session""" + auto_captcha_solving: Optional[bool] = FieldInfo(alias="autoCaptchaSolving", default=None) + """When true, captchas will be automatically solved when detected. + + When false, use the solve endpoints to manually initiate solving. + """ + humanize_interactions: Optional[bool] = FieldInfo(alias="humanizeInteractions", default=None) """ This flag will make the browser act more human-like by moving the mouse in a diff --git a/tests/api_resources/sessions/test_captchas.py b/tests/api_resources/sessions/test_captchas.py index 5b9ebdb..7eb1811 100644 --- a/tests/api_resources/sessions/test_captchas.py +++ b/tests/api_resources/sessions/test_captchas.py @@ -9,7 +9,11 @@ from steel import Steel, AsyncSteel from tests.utils import assert_matches_type -from steel.types.sessions import CaptchaStatusResponse, CaptchaSolveImageResponse +from steel.types.sessions import ( + CaptchaSolveResponse, + CaptchaStatusResponse, + CaptchaSolveImageResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -17,6 +21,54 @@ class TestCaptchas: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize + def test_method_solve(self, client: Steel) -> None: + captcha = client.sessions.captchas.solve( + session_id="sessionId", + ) + assert_matches_type(CaptchaSolveResponse, captcha, path=["response"]) + + @parametrize + def test_method_solve_with_all_params(self, client: Steel) -> None: + captcha = client.sessions.captchas.solve( + session_id="sessionId", + page_id="pageId", + task_id="taskId", + url="url", + ) + assert_matches_type(CaptchaSolveResponse, captcha, path=["response"]) + + @parametrize + def test_raw_response_solve(self, client: Steel) -> None: + response = client.sessions.captchas.with_raw_response.solve( + session_id="sessionId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + captcha = response.parse() + assert_matches_type(CaptchaSolveResponse, captcha, path=["response"]) + + @parametrize + def test_streaming_response_solve(self, client: Steel) -> None: + with client.sessions.captchas.with_streaming_response.solve( + session_id="sessionId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + captcha = response.parse() + assert_matches_type(CaptchaSolveResponse, captcha, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_solve(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( + session_id="", + ) + @parametrize def test_method_solve_image(self, client: Steel) -> None: captcha = client.sessions.captchas.solve_image( @@ -117,6 +169,54 @@ class TestAsyncCaptchas: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) + @parametrize + async def test_method_solve(self, async_client: AsyncSteel) -> None: + captcha = await async_client.sessions.captchas.solve( + session_id="sessionId", + ) + assert_matches_type(CaptchaSolveResponse, captcha, path=["response"]) + + @parametrize + async def test_method_solve_with_all_params(self, async_client: AsyncSteel) -> None: + captcha = await async_client.sessions.captchas.solve( + session_id="sessionId", + page_id="pageId", + task_id="taskId", + url="url", + ) + assert_matches_type(CaptchaSolveResponse, captcha, path=["response"]) + + @parametrize + async def test_raw_response_solve(self, async_client: AsyncSteel) -> None: + response = await async_client.sessions.captchas.with_raw_response.solve( + session_id="sessionId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + captcha = await response.parse() + assert_matches_type(CaptchaSolveResponse, captcha, path=["response"]) + + @parametrize + async def test_streaming_response_solve(self, async_client: AsyncSteel) -> None: + async with async_client.sessions.captchas.with_streaming_response.solve( + session_id="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(CaptchaSolveResponse, captcha, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_solve(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( + session_id="", + ) + @parametrize async def test_method_solve_image(self, async_client: AsyncSteel) -> None: captcha = await async_client.sessions.captchas.solve_image( diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 4284a83..05d575d 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -121,6 +121,7 @@ def test_method_create_with_all_params(self, client: Steel) -> None: session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", solve_captcha=True, stealth_config={ + "auto_captcha_solving": True, "humanize_interactions": True, "skip_fingerprint_injection": True, }, @@ -981,6 +982,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", solve_captcha=True, stealth_config={ + "auto_captcha_solving": True, "humanize_interactions": True, "skip_fingerprint_injection": True, }, From 51862eae19e4c8262568bfcfd8772df988acde91 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:09:55 +0000 Subject: [PATCH 15/15] release: 0.16.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/steel/_version.py | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8f3e0a4..b4e9013 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.15.0" + ".": "0.16.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 14fa1fd..5f1805a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 0.16.0 (2026-02-06) + +Full Changelog: [v0.15.0...v0.16.0](https://github.com/steel-dev/steel-python/compare/v0.15.0...v0.16.0) + +### Features + +* **api:** api update ([6efa7c6](https://github.com/steel-dev/steel-python/commit/6efa7c68d1783cf007c2854e095aaaa29cf70bde)) +* **api:** api update ([39bf149](https://github.com/steel-dev/steel-python/commit/39bf14915d1a92a92bf999c7d4d7cdf457f594fd)) +* **client:** add custom JSON encoder for extended type support ([9c37892](https://github.com/steel-dev/steel-python/commit/9c3789211e990081c0d97fd4684d480671942503)) +* **client:** add support for binary request streaming ([173f4bc](https://github.com/steel-dev/steel-python/commit/173f4bccb8cec99bd0cbe652203a85d200a98520)) + + +### Bug Fixes + +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([941b1cc](https://github.com/steel-dev/steel-python/commit/941b1cc3019655d765bc4e1d66a8d5eb7c66b699)) +* use async_to_httpx_files in patch method ([4d5f789](https://github.com/steel-dev/steel-python/commit/4d5f789e363954650468371049d9d9a6822d4484)) + + +### Chores + +* add missing docstrings ([f16212d](https://github.com/steel-dev/steel-python/commit/f16212dbd7d11e72e909b4c527358e4c6577d342)) +* **ci:** upgrade `actions/github-script` ([a950e2a](https://github.com/steel-dev/steel-python/commit/a950e2afb5d34aace130b42e6118ccb255639b82)) +* **internal:** add `--fix` argument to lint script ([ed11e7a](https://github.com/steel-dev/steel-python/commit/ed11e7aa7441a51314e4ca6b97489d89e34dc75a)) +* **internal:** add missing files argument to base client ([92b8e72](https://github.com/steel-dev/steel-python/commit/92b8e72a9530757a3da642154a0c9d6e00f158b8)) +* **internal:** codegen related update ([fa9cdd1](https://github.com/steel-dev/steel-python/commit/fa9cdd1963f4ef1b62c7043f6357152a1044ccf2)) +* **internal:** update `actions/checkout` version ([f226fef](https://github.com/steel-dev/steel-python/commit/f226fef740814026024c09636063a76c6e702e0c)) +* speedup initial import ([cb228f5](https://github.com/steel-dev/steel-python/commit/cb228f5f13404ed892accf61ecece2d4a62a6670)) + ## 0.15.0 (2025-12-05) Full Changelog: [v0.14.0...v0.15.0](https://github.com/steel-dev/steel-python/compare/v0.14.0...v0.15.0) diff --git a/pyproject.toml b/pyproject.toml index 6e816a7..10d2c91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "steel-sdk" -version = "0.15.0" +version = "0.16.0" description = "The official Python library for the steel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/steel/_version.py b/src/steel/_version.py index a26394e..b858098 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.15.0" # x-release-please-version +__version__ = "0.16.0" # x-release-please-version