diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 10f30916..6b7b74c5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.3.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 4b8c6758..4ec03709 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 17 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-c8306e25d5c8e0d16318b9d44a683dd3d10f8d8b3d56ecbd1952d7f9e95d7f08.yml -openapi_spec_hash: 9877212f13f31009e05d8a1f8b2dd750 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/nen-labs%2Fsteel-df307c8d4d17c23d37054bf05f4d0a154be8011730c73ef8b3cfc987bcbdc05e.yml +openapi_spec_hash: f3227dde2385091c20c58a17621286c7 config_hash: f23d5011c9a89d67725b48e96ffb7c99 diff --git a/CHANGELOG.md b/CHANGELOG.md index aad2b2f4..57da802d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.3.0 (2025-05-12) + +Full Changelog: [v0.2.0...v0.3.0](https://github.com/steel-dev/steel-python/compare/v0.2.0...v0.3.0) + +### Features + +* **api:** api update ([de03f2d](https://github.com/steel-dev/steel-python/commit/de03f2d18de91dd52b45652466a6bc9c2772a00a)) + + +### Bug Fixes + +* **package:** support direct resource imports ([6bcebb1](https://github.com/steel-dev/steel-python/commit/6bcebb13d5af14d8e6415d0d0256b22431f78e76)) + + +### Chores + +* **internal:** avoid errors for isinstance checks on proxies ([5517961](https://github.com/steel-dev/steel-python/commit/5517961f8463167e35f6e0befea81caf4d4fbfd4)) + ## 0.2.0 (2025-04-24) Full Changelog: [v0.1.0...v0.2.0](https://github.com/steel-dev/steel-python/compare/v0.1.0...v0.2.0) diff --git a/pyproject.toml b/pyproject.toml index 85fd4c9d..d86f47d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "steel-sdk" -version = "0.2.0" +version = "0.3.0" description = "The official Python library for the steel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/steel/__init__.py b/src/steel/__init__.py index a7193370..1a873730 100644 --- a/src/steel/__init__.py +++ b/src/steel/__init__.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import typing as _t + from . import types from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes from ._utils import file_from_path @@ -68,6 +70,9 @@ "DefaultAsyncHttpxClient", ] +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + _setup_logging() # Update the __module__ attribute for exported symbols so that diff --git a/src/steel/_utils/_proxy.py b/src/steel/_utils/_proxy.py index ffd883e9..0f239a33 100644 --- a/src/steel/_utils/_proxy.py +++ b/src/steel/_utils/_proxy.py @@ -46,7 +46,10 @@ def __dir__(self) -> Iterable[str]: @property # type: ignore @override def __class__(self) -> type: # pyright: ignore - proxied = self.__get_proxied__() + try: + proxied = self.__get_proxied__() + except Exception: + return type(self) if issubclass(type(proxied), LazyProxy): return type(proxied) return proxied.__class__ diff --git a/src/steel/_utils/_resources_proxy.py b/src/steel/_utils/_resources_proxy.py new file mode 100644 index 00000000..a8681ddf --- /dev/null +++ b/src/steel/_utils/_resources_proxy.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Any +from typing_extensions import override + +from ._proxy import LazyProxy + + +class ResourcesProxy(LazyProxy[Any]): + """A proxy for the `steel.resources` module. + + This is used so that we can lazily import `steel.resources` only when + needed *and* so that users can just import `steel` and reference `steel.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("steel.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() diff --git a/src/steel/_version.py b/src/steel/_version.py index 824fbb56..12bdd846 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.2.0" # x-release-please-version +__version__ = "0.3.0" # x-release-please-version diff --git a/src/steel/types/session_context.py b/src/steel/types/session_context.py index d50b1ed2..445867e3 100644 --- a/src/steel/types/session_context.py +++ b/src/steel/types/session_context.py @@ -1,13 +1,36 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Dict, List, Optional +from datetime import datetime from typing_extensions import Literal from pydantic import Field as FieldInfo from .._models import BaseModel -__all__ = ["SessionContext", "Cookie"] +__all__ = [ + "SessionContext", + "Cookie", + "CookiePartitionKey", + "IndexedDB", + "IndexedDBData", + "IndexedDBDataRecord", + "IndexedDBDataRecordBlobFile", +] + + +class CookiePartitionKey(BaseModel): + has_cross_site_ancestor: bool = FieldInfo(alias="hasCrossSiteAncestor") + """ + Indicates if the cookie has any ancestors that are cross-site to the + topLevelSite. + """ + + top_level_site: str = FieldInfo(alias="topLevelSite") + """ + The site of the top-level URL the browser was visiting at the start of the + request to the endpoint that set the cookie. + """ class Cookie(BaseModel): @@ -26,7 +49,7 @@ class Cookie(BaseModel): http_only: Optional[bool] = FieldInfo(alias="httpOnly", default=None) """Whether the cookie is HTTP only""" - partition_key: Optional[str] = FieldInfo(alias="partitionKey", default=None) + partition_key: Optional[CookiePartitionKey] = FieldInfo(alias="partitionKey", default=None) """The partition key of the cookie""" path: Optional[str] = None @@ -60,15 +83,53 @@ class Cookie(BaseModel): """The URL of the cookie""" +class IndexedDBDataRecordBlobFile(BaseModel): + blob_number: float = FieldInfo(alias="blobNumber") + + mime_type: str = FieldInfo(alias="mimeType") + + size: float + + filename: Optional[str] = None + + last_modified: Optional[datetime] = FieldInfo(alias="lastModified", default=None) + + path: Optional[str] = None + + +class IndexedDBDataRecord(BaseModel): + blob_files: Optional[List[IndexedDBDataRecordBlobFile]] = FieldInfo(alias="blobFiles", default=None) + + key: Optional[object] = None + + value: Optional[object] = None + + +class IndexedDBData(BaseModel): + id: float + + name: str + + records: List[IndexedDBDataRecord] + + +class IndexedDB(BaseModel): + id: float + + data: List[IndexedDBData] + + name: str + + class SessionContext(BaseModel): cookies: Optional[List[Cookie]] = None """Cookies to initialize in the session""" - indexed_db: Optional[Dict[str, List[Dict[str, object]]]] = FieldInfo(alias="indexedDB", default=None) + indexed_db: Optional[Dict[str, List[IndexedDB]]] = FieldInfo(alias="indexedDB", default=None) """Domain-specific indexedDB items to initialize in the session""" - local_storage: Optional[Dict[str, Dict[str, object]]] = FieldInfo(alias="localStorage", default=None) + local_storage: Optional[Dict[str, Dict[str, str]]] = FieldInfo(alias="localStorage", default=None) """Domain-specific localStorage items to initialize in the session""" - session_storage: Optional[Dict[str, Dict[str, object]]] = FieldInfo(alias="sessionStorage", default=None) + session_storage: Optional[Dict[str, Dict[str, str]]] = FieldInfo(alias="sessionStorage", default=None) """Domain-specific sessionStorage items to initialize in the session""" diff --git a/src/steel/types/session_create_params.py b/src/steel/types/session_create_params.py index 2b1643f2..862e5895 100644 --- a/src/steel/types/session_create_params.py +++ b/src/steel/types/session_create_params.py @@ -2,12 +2,23 @@ from __future__ import annotations -from typing import Dict, Iterable +from typing import Dict, Union, Iterable +from datetime import datetime from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo -__all__ = ["SessionCreateParams", "Dimensions", "SessionContext", "SessionContextCookie"] +__all__ = [ + "SessionCreateParams", + "Dimensions", + "SessionContext", + "SessionContextCookie", + "SessionContextCookiePartitionKey", + "SessionContextIndexedDB", + "SessionContextIndexedDBData", + "SessionContextIndexedDBDataRecord", + "SessionContextIndexedDBDataRecordBlobFile", +] class SessionCreateParams(TypedDict, total=False): @@ -66,6 +77,20 @@ class Dimensions(TypedDict, total=False): """Width of the session""" +class SessionContextCookiePartitionKey(TypedDict, total=False): + has_cross_site_ancestor: Required[Annotated[bool, PropertyInfo(alias="hasCrossSiteAncestor")]] + """ + Indicates if the cookie has any ancestors that are cross-site to the + topLevelSite. + """ + + top_level_site: Required[Annotated[str, PropertyInfo(alias="topLevelSite")]] + """ + The site of the top-level URL the browser was visiting at the start of the + request to the endpoint that set the cookie. + """ + + class SessionContextCookie(TypedDict, total=False): name: Required[str] """The name of the cookie""" @@ -82,7 +107,7 @@ class SessionContextCookie(TypedDict, total=False): http_only: Annotated[bool, PropertyInfo(alias="httpOnly")] """Whether the cookie is HTTP only""" - partition_key: Annotated[str, PropertyInfo(alias="partitionKey")] + partition_key: Annotated[SessionContextCookiePartitionKey, PropertyInfo(alias="partitionKey")] """The partition key of the cookie""" path: str @@ -116,15 +141,53 @@ class SessionContextCookie(TypedDict, total=False): """The URL of the cookie""" +class SessionContextIndexedDBDataRecordBlobFile(TypedDict, total=False): + blob_number: Required[Annotated[float, PropertyInfo(alias="blobNumber")]] + + mime_type: Required[Annotated[str, PropertyInfo(alias="mimeType")]] + + size: Required[float] + + filename: str + + last_modified: Annotated[Union[str, datetime], PropertyInfo(alias="lastModified", format="iso8601")] + + path: str + + +class SessionContextIndexedDBDataRecord(TypedDict, total=False): + blob_files: Annotated[Iterable[SessionContextIndexedDBDataRecordBlobFile], PropertyInfo(alias="blobFiles")] + + key: object + + value: object + + +class SessionContextIndexedDBData(TypedDict, total=False): + id: Required[float] + + name: Required[str] + + records: Required[Iterable[SessionContextIndexedDBDataRecord]] + + +class SessionContextIndexedDB(TypedDict, total=False): + id: Required[float] + + data: Required[Iterable[SessionContextIndexedDBData]] + + name: Required[str] + + class SessionContext(TypedDict, total=False): cookies: Iterable[SessionContextCookie] """Cookies to initialize in the session""" - indexed_db: Annotated[Dict[str, Iterable[Dict[str, object]]], PropertyInfo(alias="indexedDB")] + indexed_db: Annotated[Dict[str, Iterable[SessionContextIndexedDB]], PropertyInfo(alias="indexedDB")] """Domain-specific indexedDB items to initialize in the session""" - local_storage: Annotated[Dict[str, Dict[str, object]], PropertyInfo(alias="localStorage")] + local_storage: Annotated[Dict[str, Dict[str, str]], PropertyInfo(alias="localStorage")] """Domain-specific localStorage items to initialize in the session""" - session_storage: Annotated[Dict[str, Dict[str, object]], PropertyInfo(alias="sessionStorage")] + session_storage: Annotated[Dict[str, Dict[str, str]], PropertyInfo(alias="sessionStorage")] """Domain-specific sessionStorage items to initialize in the session""" diff --git a/tests/api_resources/test_sessions.py b/tests/api_resources/test_sessions.py index 55c9fed8..0d263eb1 100644 --- a/tests/api_resources/test_sessions.py +++ b/tests/api_resources/test_sessions.py @@ -17,6 +17,7 @@ SessionLiveDetailsResponse, ) from tests.utils import assert_matches_type +from steel._utils import parse_datetime from steel.pagination import SyncSessionsCursor, AsyncSessionsCursor from steel.types.sessionslist import Session as SessionslistSession @@ -41,7 +42,7 @@ def test_method_create_with_all_params(self, client: Steel) -> None: "width": 0, }, is_selenium=True, - proxy_url="proxyUrl", + proxy_url="https://example.com", session_context={ "cookies": [ { @@ -50,7 +51,10 @@ def test_method_create_with_all_params(self, client: Steel) -> None: "domain": "domain", "expires": 0, "http_only": True, - "partition_key": "partitionKey", + "partition_key": { + "has_cross_site_ancestor": True, + "top_level_site": "topLevelSite", + }, "path": "path", "priority": "Low", "same_party": True, @@ -63,9 +67,38 @@ def test_method_create_with_all_params(self, client: Steel) -> None: "url": "url", } ], - "indexed_db": {"foo": [{"foo": "bar"}]}, - "local_storage": {"foo": {"foo": "bar"}}, - "session_storage": {"foo": {"foo": "bar"}}, + "indexed_db": { + "foo": [ + { + "id": 0, + "data": [ + { + "id": 0, + "name": "name", + "records": [ + { + "blob_files": [ + { + "blob_number": 0, + "mime_type": "mimeType", + "size": 0, + "filename": "filename", + "last_modified": parse_datetime("2019-12-27T18:11:19.117Z"), + "path": "path", + } + ], + "key": {}, + "value": {}, + } + ], + } + ], + "name": "name", + } + ] + }, + "local_storage": {"foo": {"foo": "string"}}, + "session_storage": {"foo": {"foo": "string"}}, }, session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", solve_captcha=True, @@ -363,7 +396,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> "width": 0, }, is_selenium=True, - proxy_url="proxyUrl", + proxy_url="https://example.com", session_context={ "cookies": [ { @@ -372,7 +405,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> "domain": "domain", "expires": 0, "http_only": True, - "partition_key": "partitionKey", + "partition_key": { + "has_cross_site_ancestor": True, + "top_level_site": "topLevelSite", + }, "path": "path", "priority": "Low", "same_party": True, @@ -385,9 +421,38 @@ async def test_method_create_with_all_params(self, async_client: AsyncSteel) -> "url": "url", } ], - "indexed_db": {"foo": [{"foo": "bar"}]}, - "local_storage": {"foo": {"foo": "bar"}}, - "session_storage": {"foo": {"foo": "bar"}}, + "indexed_db": { + "foo": [ + { + "id": 0, + "data": [ + { + "id": 0, + "name": "name", + "records": [ + { + "blob_files": [ + { + "blob_number": 0, + "mime_type": "mimeType", + "size": 0, + "filename": "filename", + "last_modified": parse_datetime("2019-12-27T18:11:19.117Z"), + "path": "path", + } + ], + "key": {}, + "value": {}, + } + ], + } + ], + "name": "name", + } + ] + }, + "local_storage": {"foo": {"foo": "string"}}, + "session_storage": {"foo": {"foo": "string"}}, }, session_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", solve_captcha=True, diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py index 2bb0a368..80987e3b 100644 --- a/tests/test_utils/test_proxy.py +++ b/tests/test_utils/test_proxy.py @@ -21,3 +21,14 @@ def test_recursive_proxy() -> None: assert dir(proxy) == [] assert type(proxy).__name__ == "RecursiveLazyProxy" assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" + + +def test_isinstance_does_not_error() -> None: + class AlwaysErrorProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + raise RuntimeError("Mocking missing dependency") + + proxy = AlwaysErrorProxy() + assert not isinstance(proxy, dict) + assert isinstance(proxy, LazyProxy)