diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8814d63..8ba6b37 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: |
@@ -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());
@@ -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: |
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/.stats.yml b/.stats.yml
index 419210f..416f1d7 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
-config_hash: e49b3f69d57d7ffa0420acf772d5c846
+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/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/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.
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..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
@@ -129,10 +134,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/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/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'
diff --git a/src/steel/_base_client.py b/src/steel/_base_client.py
index c03de41..278ca50 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,
@@ -83,6 +86,7 @@
APIConnectionError,
APIResponseValidationError,
)
+from ._utils._json import openapi_dumps
log: logging.Logger = logging.getLogger(__name__)
@@ -477,8 +481,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,10 +547,18 @@ 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
+ 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)
@@ -1194,6 +1217,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 +1230,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 +1244,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 +1257,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,9 +1285,24 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", 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 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, content=content, files=to_httpx_files(files), **options
+ )
return self.request(cast_to, opts)
def put(
@@ -1258,11 +1311,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)
@@ -1272,9 +1337,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(
@@ -1714,6 +1789,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,
@@ -1726,6 +1802,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],
@@ -1739,6 +1816,7 @@ async def post(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
files: RequestFiles | None = None,
options: RequestOptions = {},
stream: bool,
@@ -1751,13 +1829,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)
@@ -1767,9 +1857,29 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", 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 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,
+ content=content,
+ files=await async_to_httpx_files(files),
+ **options,
+ )
return await self.request(cast_to, opts)
async def put(
@@ -1778,11 +1888,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)
@@ -1792,9 +1914,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/_client.py b/src/steel/_client.py
index 9ceee7e..eb20451 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
@@ -199,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.
@@ -252,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.
@@ -314,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.
@@ -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
@@ -542,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.
@@ -595,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.
@@ -657,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.
@@ -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
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/_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/_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 4a0df7c..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,
@@ -243,6 +252,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 +263,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
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/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
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/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/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_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/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 eb20bef..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,11 +23,10 @@ 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"""
+
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..f2b58c6 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
@@ -478,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"""
@@ -496,11 +503,8 @@ class ProfileCreateResponse(BaseModel):
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..f8cd486
--- /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"]
+ """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 9c6a445..1d02923 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
@@ -479,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"""
@@ -497,15 +504,12 @@ class Profile(BaseModel):
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..909f8f8
--- /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"]
+ """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/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..0aaf198 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,20 @@ 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"""
+
+ 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
@@ -61,6 +80,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..4e55302 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
@@ -98,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):
@@ -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..b98d7f3 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,16 +121,15 @@ 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"""
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 +138,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 +158,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 +192,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
@@ -259,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):
@@ -283,6 +296,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 +315,14 @@ 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
@@ -308,6 +334,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_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/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/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/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 53833ed..05604e7 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,20 @@ 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"""
+
+ 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
@@ -69,6 +88,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 +169,13 @@ 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_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/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..05d575d 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": {},
}
],
}
@@ -121,10 +121,11 @@ 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,
},
- api_timeout=0,
+ api_timeout=-9007199254740991,
use_proxy=True,
user_agent="userAgent",
)
@@ -197,7 +198,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 +898,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 +910,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 +921,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 +956,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 +968,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) ->
"path": "path",
}
],
- "key": {},
- "value": {},
}
],
}
@@ -981,10 +982,11 @@ 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,
},
- api_timeout=0,
+ api_timeout=-9007199254740991,
use_proxy=True,
user_agent="userAgent",
)
@@ -1057,7 +1059,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"])
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):
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}}'