diff --git a/.release-please-manifest.json b/.release-please-manifest.json index da59f99..2aca35a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.0" + ".": "0.5.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index be606c6..d654666 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 11 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-64ccdff4ca5d73d79d89e817fe83ccfd3d529696df3e6818c3c75e586ae00801.yml -openapi_spec_hash: 21c7b8757fc0cc9415cda1bc06251de6 -config_hash: b3fcacd707da56b21d31ce0baf4fb87d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-4502c65bef0843a6ae96d23bba075433af6bab49b55b544b1522f63e7881c00c.yml +openapi_spec_hash: 3e67b77bbc8cd6155b8f66f3271f2643 +config_hash: c6bab7ac8da570a5abbcfb19db119b6b diff --git a/CHANGELOG.md b/CHANGELOG.md index db6db72..462f762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.5.0 (2025-06-03) + +Full Changelog: [v0.4.0...v0.5.0](https://github.com/onkernel/kernel-python-sdk/compare/v0.4.0...v0.5.0) + +### Features + +* **api:** update via SDK Studio ([6bc85d1](https://github.com/onkernel/kernel-python-sdk/commit/6bc85d1fb74d7c496c02c1bde19129ae07892af7)) +* **api:** update via SDK Studio ([007cb3c](https://github.com/onkernel/kernel-python-sdk/commit/007cb3cafc3697743131489bfd46651f246c2e87)) +* **client:** add follow_redirects request option ([4db3b7f](https://github.com/onkernel/kernel-python-sdk/commit/4db3b7f7a19af62ac986fcf4482cfe0a5454ca50)) + + +### Chores + +* **docs:** remove reference to rye shell ([1f9ea78](https://github.com/onkernel/kernel-python-sdk/commit/1f9ea78913d336137e76aa4d8c83e708ee6b9fd6)) + ## 0.4.0 (2025-05-28) Full Changelog: [v0.3.0...v0.4.0](https://github.com/onkernel/kernel-python-sdk/compare/v0.3.0...v0.4.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c486484..f05c930 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,8 +17,7 @@ $ rye sync --all-features You can then run scripts using `rye run python script.py` or by activating the virtual environment: ```sh -$ rye shell -# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work $ source .venv/bin/activate # now you can omit the `rye run` prefix diff --git a/pyproject.toml b/pyproject.toml index ed0ec2d..4c9fc23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.4.0" +version = "0.5.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_base_client.py b/src/kernel/_base_client.py index 34308dd..785adea 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -960,6 +960,9 @@ def request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None @@ -1460,6 +1463,9 @@ async def request( if self.custom_auth is not None: kwargs["auth"] = self.custom_auth + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + log.debug("Sending HTTP Request: %s %s", request.method, request.url) response = None diff --git a/src/kernel/_models.py b/src/kernel/_models.py index 798956f..4f21498 100644 --- a/src/kernel/_models.py +++ b/src/kernel/_models.py @@ -737,6 +737,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): idempotency_key: str json_data: Body extra_json: AnyMapping + follow_redirects: bool @final @@ -750,6 +751,7 @@ class FinalRequestOptions(pydantic.BaseModel): files: Union[HttpxRequestFiles, None] = None idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + follow_redirects: Union[bool, None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. diff --git a/src/kernel/_types.py b/src/kernel/_types.py index 2b0c5c3..18a1ef5 100644 --- a/src/kernel/_types.py +++ b/src/kernel/_types.py @@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False): params: Query extra_json: AnyMapping idempotency_key: str + follow_redirects: bool # Sentinel class used until PEP 0661 is accepted @@ -215,3 +216,4 @@ class _GenericAlias(Protocol): class HttpxSendArgs(TypedDict, total=False): auth: httpx.Auth + follow_redirects: bool diff --git a/src/kernel/_version.py b/src/kernel/_version.py index e201745..2c947c2 100644 --- a/src/kernel/_version.py +++ b/src/kernel/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "kernel" -__version__ = "0.4.0" # x-release-please-version +__version__ = "0.5.0" # x-release-please-version diff --git a/src/kernel/resources/apps/apps.py b/src/kernel/resources/apps/apps.py index 9a5f667..3769bd5 100644 --- a/src/kernel/resources/apps/apps.py +++ b/src/kernel/resources/apps/apps.py @@ -77,10 +77,9 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AppListResponse: - """List application versions for the authenticated user. + """List applications. - Optionally filter by app - name and/or version label. + Optionally filter by app name and/or version label. Args: app_name: Filter results by application name. @@ -154,10 +153,9 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> AppListResponse: - """List application versions for the authenticated user. + """List applications. - Optionally filter by app - name and/or version label. + Optionally filter by app name and/or version label. Args: app_name: Filter results by application name. diff --git a/src/kernel/resources/apps/deployments.py b/src/kernel/resources/apps/deployments.py index a3e364a..98d1728 100644 --- a/src/kernel/resources/apps/deployments.py +++ b/src/kernel/resources/apps/deployments.py @@ -63,7 +63,7 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> DeploymentCreateResponse: """ - Deploy a new application + Deploy a new application and associated actions to Kernel. Args: entrypoint_rel_path: Relative path to the entrypoint of the application @@ -190,7 +190,7 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> DeploymentCreateResponse: """ - Deploy a new application + Deploy a new application and associated actions to Kernel. Args: entrypoint_rel_path: Relative path to the entrypoint of the application diff --git a/src/kernel/resources/apps/invocations.py b/src/kernel/resources/apps/invocations.py index 3f1f495..b5413d4 100644 --- a/src/kernel/resources/apps/invocations.py +++ b/src/kernel/resources/apps/invocations.py @@ -61,7 +61,7 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationCreateResponse: """ - Invoke an application + Invoke an action. Args: action_name: Name of the action to invoke @@ -113,7 +113,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationRetrieveResponse: """ - Get an app invocation by id + Get details about an invocation's status and output. Args: extra_headers: Send extra headers @@ -148,7 +148,7 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationUpdateResponse: """ - Update invocation status or output + Update an invocation's status or output. Args: status: New status for the invocation. @@ -217,7 +217,7 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationCreateResponse: """ - Invoke an application + Invoke an action. Args: action_name: Name of the action to invoke @@ -269,7 +269,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationRetrieveResponse: """ - Get an app invocation by id + Get details about an invocation's status and output. Args: extra_headers: Send extra headers @@ -304,7 +304,7 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> InvocationUpdateResponse: """ - Update invocation status or output + Update an invocation's status or output. Args: status: New status for the invocation. diff --git a/src/kernel/resources/browsers.py b/src/kernel/resources/browsers.py index 6816edd..e3dc833 100644 --- a/src/kernel/resources/browsers.py +++ b/src/kernel/resources/browsers.py @@ -49,6 +49,7 @@ def create( *, invocation_id: str, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + stealth: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -57,13 +58,16 @@ def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserCreateResponse: """ - Create Browser Session + Create a new browser session from within an action. Args: invocation_id: action invocation ID persistence: Optional persistence configuration for the browser session. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -78,6 +82,7 @@ def create( { "invocation_id": invocation_id, "persistence": persistence, + "stealth": stealth, }, browser_create_params.BrowserCreateParams, ), @@ -99,7 +104,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserRetrieveResponse: """ - Get Browser Session by ID + Get information about a browser session. Args: extra_headers: Send extra headers @@ -130,7 +135,7 @@ def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserListResponse: - """List active browser sessions for the authenticated user""" + """List active browser sessions""" return self._get( "/browsers", options=make_request_options( @@ -151,7 +156,7 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete a persistent browser session by persistent_id query parameter. + Delete a persistent browser session by its persistent_id. Args: persistent_id: Persistent browser identifier @@ -189,7 +194,7 @@ def delete_by_id( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete Browser Session by ID + Delete a browser session by ID Args: extra_headers: Send extra headers @@ -237,6 +242,7 @@ async def create( *, invocation_id: str, persistence: BrowserPersistenceParam | NotGiven = NOT_GIVEN, + stealth: bool | NotGiven = NOT_GIVEN, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -245,13 +251,16 @@ async def create( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserCreateResponse: """ - Create Browser Session + Create a new browser session from within an action. Args: invocation_id: action invocation ID persistence: Optional persistence configuration for the browser session. + stealth: If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -266,6 +275,7 @@ async def create( { "invocation_id": invocation_id, "persistence": persistence, + "stealth": stealth, }, browser_create_params.BrowserCreateParams, ), @@ -287,7 +297,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserRetrieveResponse: """ - Get Browser Session by ID + Get information about a browser session. Args: extra_headers: Send extra headers @@ -318,7 +328,7 @@ async def list( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> BrowserListResponse: - """List active browser sessions for the authenticated user""" + """List active browser sessions""" return await self._get( "/browsers", options=make_request_options( @@ -339,7 +349,7 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete a persistent browser session by persistent_id query parameter. + Delete a persistent browser session by its persistent_id. Args: persistent_id: Persistent browser identifier @@ -379,7 +389,7 @@ async def delete_by_id( timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, ) -> None: """ - Delete Browser Session by ID + Delete a browser session by ID Args: extra_headers: Send extra headers diff --git a/src/kernel/types/browser_create_params.py b/src/kernel/types/browser_create_params.py index 14fd5fe..e50aefb 100644 --- a/src/kernel/types/browser_create_params.py +++ b/src/kernel/types/browser_create_params.py @@ -15,3 +15,9 @@ class BrowserCreateParams(TypedDict, total=False): persistence: BrowserPersistenceParam """Optional persistence configuration for the browser session.""" + + stealth: bool + """ + If true, launches the browser in stealth mode to reduce detection by anti-bot + mechanisms. + """ diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py index 260d171..4593d2f 100644 --- a/tests/api_resources/test_browsers.py +++ b/tests/api_resources/test_browsers.py @@ -35,6 +35,7 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: browser = client.browsers.create( invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) @@ -228,6 +229,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> browser = await async_client.browsers.create( invocation_id="rr33xuugxj9h0bkf1rdt2bet", persistence={"id": "my-awesome-browser-for-user-1234"}, + stealth=True, ) assert_matches_type(BrowserCreateResponse, browser, path=["response"]) diff --git a/tests/test_client.py b/tests/test_client.py index 5c906fc..3a6dcfb 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -837,6 +837,33 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: assert response.http_request.headers.get("x-stainless-retry-count") == "42" + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + class TestAsyncKernel: client = AsyncKernel(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1688,3 +1715,30 @@ async def test_main() -> None: raise AssertionError("calling get_platform using asyncify resulted in a hung process") time.sleep(0.1) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await self.client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"