From 50555c636b2c733c731a04be855b962168d050b1 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:25:28 +0000 Subject: [PATCH 1/3] Build SDK Stainless-Generated-From: d614e63f2e20dff8ad131ca6220aa0b97fc37248 --- src/kernel/types/shared/browser_viewport.py | 10 ++++++++-- src/kernel/types/shared_params/browser_viewport.py | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/kernel/types/shared/browser_viewport.py b/src/kernel/types/shared/browser_viewport.py index c2d85101..6d856c81 100644 --- a/src/kernel/types/shared/browser_viewport.py +++ b/src/kernel/types/shared/browser_viewport.py @@ -23,10 +23,16 @@ class BrowserViewport(BaseModel): """ height: int - """Browser window height in pixels. Any positive integer is accepted.""" + """Browser window height in pixels. + + Any positive integer is accepted; headful browsers require at least 64. + """ width: int - """Browser window width in pixels. Any positive integer is accepted.""" + """Browser window width in pixels. + + Any positive integer is accepted; headful browsers require at least 64. + """ refresh_rate: Optional[int] = None """Display refresh rate in Hz. diff --git a/src/kernel/types/shared_params/browser_viewport.py b/src/kernel/types/shared_params/browser_viewport.py index 3f31ee7f..47773f3a 100644 --- a/src/kernel/types/shared_params/browser_viewport.py +++ b/src/kernel/types/shared_params/browser_viewport.py @@ -23,10 +23,16 @@ class BrowserViewport(TypedDict, total=False): """ height: Required[int] - """Browser window height in pixels. Any positive integer is accepted.""" + """Browser window height in pixels. + + Any positive integer is accepted; headful browsers require at least 64. + """ width: Required[int] - """Browser window width in pixels. Any positive integer is accepted.""" + """Browser window width in pixels. + + Any positive integer is accepted; headful browsers require at least 64. + """ refresh_rate: int """Display refresh rate in Hz. From cfda76aba98f670654ecb40853a33e8bfaeeb82e Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:18:10 +0000 Subject: [PATCH 2/3] feat: Let reserved orgs list, pin and move ISP proxy IPs Stainless-Generated-From: e45141397c218a32003b28e1e2bb9077979936be --- .stats.yml | 2 +- api.md | 13 + src/kernel/_client.py | 44 +++ src/kernel/resources/__init__.py | 14 + src/kernel/resources/proxies.py | 60 +++- src/kernel/resources/proxy_ips.py | 319 ++++++++++++++++++++ src/kernel/types/__init__.py | 3 + src/kernel/types/proxy.py | 14 +- src/kernel/types/proxy_check_response.py | 14 +- src/kernel/types/proxy_create_params.py | 14 +- src/kernel/types/proxy_create_response.py | 14 +- src/kernel/types/proxy_ip.py | 24 ++ src/kernel/types/proxy_ip_detail.py | 45 +++ src/kernel/types/proxy_ip_list_params.py | 21 ++ src/kernel/types/proxy_list_params.py | 7 + src/kernel/types/proxy_list_response.py | 14 +- src/kernel/types/proxy_retrieve_response.py | 14 +- src/kernel/types/proxy_update_params.py | 21 +- src/kernel/types/proxy_update_response.py | 14 +- tests/api_resources/test_proxies.py | 36 ++- tests/api_resources/test_proxy_ips.py | 187 ++++++++++++ 21 files changed, 866 insertions(+), 28 deletions(-) create mode 100644 src/kernel/resources/proxy_ips.py create mode 100644 src/kernel/types/proxy_ip.py create mode 100644 src/kernel/types/proxy_ip_detail.py create mode 100644 src/kernel/types/proxy_ip_list_params.py create mode 100644 tests/api_resources/test_proxy_ips.py diff --git a/.stats.yml b/.stats.yml index ea0d5273..4725ed91 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 176 +configured_endpoints: 178 diff --git a/api.md b/api.md index 573bbb52..dada4a8b 100644 --- a/api.md +++ b/api.md @@ -498,6 +498,19 @@ Methods: - client.proxies.delete(id_or_name) -> None - client.proxies.check(id_or_name, \*\*params) -> ProxyCheckResponse +# ProxyIPs + +Types: + +```python +from kernel.types import ProxyIP, ProxyIPDetail +``` + +Methods: + +- client.proxy_ips.retrieve(ip_address) -> ProxyIPDetail +- client.proxy_ips.list(\*\*params) -> SyncOffsetPagination[ProxyIP] + # Extensions Types: diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 66394d5a..1d347980 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -64,6 +64,7 @@ browsers, profiles, projects, + proxy_ips, telemetry, audit_logs, extensions, @@ -81,6 +82,7 @@ from .resources.api_keys import APIKeysResource, AsyncAPIKeysResource from .resources.profiles import ProfilesResource, AsyncProfilesResource from .resources.auth.auth import AuthResource, AsyncAuthResource + from .resources.proxy_ips import ProxyIPsResource, AsyncProxyIPsResource from .resources.extensions import ExtensionsResource, AsyncExtensionsResource from .resources.credentials import CredentialsResource, AsyncCredentialsResource from .resources.deployments import DeploymentsResource, AsyncDeploymentsResource @@ -281,6 +283,13 @@ def proxies(self) -> ProxiesResource: return ProxiesResource(self) + @cached_property + def proxy_ips(self) -> ProxyIPsResource: + """Create and manage proxy configurations for routing browser traffic.""" + from .resources.proxy_ips import ProxyIPsResource + + return ProxyIPsResource(self) + @cached_property def extensions(self) -> ExtensionsResource: """Create, list, retrieve, and delete browser extensions.""" @@ -690,6 +699,13 @@ def proxies(self) -> AsyncProxiesResource: return AsyncProxiesResource(self) + @cached_property + def proxy_ips(self) -> AsyncProxyIPsResource: + """Create and manage proxy configurations for routing browser traffic.""" + from .resources.proxy_ips import AsyncProxyIPsResource + + return AsyncProxyIPsResource(self) + @cached_property def extensions(self) -> AsyncExtensionsResource: """Create, list, retrieve, and delete browser extensions.""" @@ -999,6 +1015,13 @@ def proxies(self) -> proxies.ProxiesResourceWithRawResponse: return ProxiesResourceWithRawResponse(self._client.proxies) + @cached_property + def proxy_ips(self) -> proxy_ips.ProxyIPsResourceWithRawResponse: + """Create and manage proxy configurations for routing browser traffic.""" + from .resources.proxy_ips import ProxyIPsResourceWithRawResponse + + return ProxyIPsResourceWithRawResponse(self._client.proxy_ips) + @cached_property def extensions(self) -> extensions.ExtensionsResourceWithRawResponse: """Create, list, retrieve, and delete browser extensions.""" @@ -1140,6 +1163,13 @@ def proxies(self) -> proxies.AsyncProxiesResourceWithRawResponse: return AsyncProxiesResourceWithRawResponse(self._client.proxies) + @cached_property + def proxy_ips(self) -> proxy_ips.AsyncProxyIPsResourceWithRawResponse: + """Create and manage proxy configurations for routing browser traffic.""" + from .resources.proxy_ips import AsyncProxyIPsResourceWithRawResponse + + return AsyncProxyIPsResourceWithRawResponse(self._client.proxy_ips) + @cached_property def extensions(self) -> extensions.AsyncExtensionsResourceWithRawResponse: """Create, list, retrieve, and delete browser extensions.""" @@ -1281,6 +1311,13 @@ def proxies(self) -> proxies.ProxiesResourceWithStreamingResponse: return ProxiesResourceWithStreamingResponse(self._client.proxies) + @cached_property + def proxy_ips(self) -> proxy_ips.ProxyIPsResourceWithStreamingResponse: + """Create and manage proxy configurations for routing browser traffic.""" + from .resources.proxy_ips import ProxyIPsResourceWithStreamingResponse + + return ProxyIPsResourceWithStreamingResponse(self._client.proxy_ips) + @cached_property def extensions(self) -> extensions.ExtensionsResourceWithStreamingResponse: """Create, list, retrieve, and delete browser extensions.""" @@ -1422,6 +1459,13 @@ def proxies(self) -> proxies.AsyncProxiesResourceWithStreamingResponse: return AsyncProxiesResourceWithStreamingResponse(self._client.proxies) + @cached_property + def proxy_ips(self) -> proxy_ips.AsyncProxyIPsResourceWithStreamingResponse: + """Create and manage proxy configurations for routing browser traffic.""" + from .resources.proxy_ips import AsyncProxyIPsResourceWithStreamingResponse + + return AsyncProxyIPsResourceWithStreamingResponse(self._client.proxy_ips) + @cached_property def extensions(self) -> extensions.AsyncExtensionsResourceWithStreamingResponse: """Create, list, retrieve, and delete browser extensions.""" diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 4be8e2ba..0827e925 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -72,6 +72,14 @@ ProjectsResourceWithStreamingResponse, AsyncProjectsResourceWithStreamingResponse, ) +from .proxy_ips import ( + ProxyIPsResource, + AsyncProxyIPsResource, + ProxyIPsResourceWithRawResponse, + AsyncProxyIPsResourceWithRawResponse, + ProxyIPsResourceWithStreamingResponse, + AsyncProxyIPsResourceWithStreamingResponse, +) from .telemetry import ( TelemetryResource, AsyncTelemetryResource, @@ -216,6 +224,12 @@ "AsyncProxiesResourceWithRawResponse", "ProxiesResourceWithStreamingResponse", "AsyncProxiesResourceWithStreamingResponse", + "ProxyIPsResource", + "AsyncProxyIPsResource", + "ProxyIPsResourceWithRawResponse", + "AsyncProxyIPsResourceWithRawResponse", + "ProxyIPsResourceWithStreamingResponse", + "AsyncProxyIPsResourceWithStreamingResponse", "ExtensionsResource", "AsyncExtensionsResource", "ExtensionsResourceWithRawResponse", diff --git a/src/kernel/resources/proxies.py b/src/kernel/resources/proxies.py index d135b3b8..98f13fb2 100644 --- a/src/kernel/resources/proxies.py +++ b/src/kernel/resources/proxies.py @@ -150,7 +150,8 @@ def update( self, id_or_name: str, *, - name: str, + config: proxy_update_params.Config | Omit = omit, + name: 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, @@ -159,12 +160,19 @@ def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> ProxyUpdateResponse: - """Update a proxy's name. + """Update a proxy's name, or move an ISP proxy to another reserved exit IP. - Proxy names are not unique and duplicate names are - allowed. + Proxy + names are not unique and duplicate names are allowed. Moving requires a reserved + ISP allocation and is accepted only for proxies of type isp: the new endpoint is + probed first and nothing changes if the probe fails. An IP outside the + allocation is a 400 with code ip_not_reserved. A 409 means the IP cannot + currently be assigned or the proxy was changed concurrently; read it back and + retry. Args: + config: Changes to an ISP proxy's exit IP. Accepted only for proxies of type `isp`. + name: New proxy name. Proxy names are trimmed and length-checked only; duplicates are allowed because proxies are updated by ID, not by name. @@ -182,7 +190,13 @@ def update( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return self._patch( path_template("/proxies/{id_or_name}", id_or_name=id_or_name), - body=maybe_transform({"name": name}, proxy_update_params.ProxyUpdateParams), + body=maybe_transform( + { + "config": config, + "name": name, + }, + proxy_update_params.ProxyUpdateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -196,6 +210,7 @@ def update( def list( self, *, + ip_address: str | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, offset: int | Omit = omit, @@ -211,6 +226,10 @@ def list( List proxies in the resolved project. Args: + ip_address: Exact match on the primary exit IP of ISP proxies. Must be an IPv4 address. Only + ISP proxies are matched: the ip_address shown on other proxy types is a one-time + observation, not a stable identity. + limit: Limit the number of proxies to return. name: Exact-match filter on proxy name using the database collation. In production, @@ -240,6 +259,7 @@ def list( timeout=timeout, query=maybe_transform( { + "ip_address": ip_address, "limit": limit, "name": name, "offset": offset, @@ -480,7 +500,8 @@ async def update( self, id_or_name: str, *, - name: str, + config: proxy_update_params.Config | Omit = omit, + name: 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, @@ -489,12 +510,19 @@ async def update( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> ProxyUpdateResponse: - """Update a proxy's name. + """Update a proxy's name, or move an ISP proxy to another reserved exit IP. - Proxy names are not unique and duplicate names are - allowed. + Proxy + names are not unique and duplicate names are allowed. Moving requires a reserved + ISP allocation and is accepted only for proxies of type isp: the new endpoint is + probed first and nothing changes if the probe fails. An IP outside the + allocation is a 400 with code ip_not_reserved. A 409 means the IP cannot + currently be assigned or the proxy was changed concurrently; read it back and + retry. Args: + config: Changes to an ISP proxy's exit IP. Accepted only for proxies of type `isp`. + name: New proxy name. Proxy names are trimmed and length-checked only; duplicates are allowed because proxies are updated by ID, not by name. @@ -512,7 +540,13 @@ async def update( raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}") return await self._patch( path_template("/proxies/{id_or_name}", id_or_name=id_or_name), - body=await async_maybe_transform({"name": name}, proxy_update_params.ProxyUpdateParams), + body=await async_maybe_transform( + { + "config": config, + "name": name, + }, + proxy_update_params.ProxyUpdateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -526,6 +560,7 @@ async def update( def list( self, *, + ip_address: str | Omit = omit, limit: int | Omit = omit, name: str | Omit = omit, offset: int | Omit = omit, @@ -541,6 +576,10 @@ def list( List proxies in the resolved project. Args: + ip_address: Exact match on the primary exit IP of ISP proxies. Must be an IPv4 address. Only + ISP proxies are matched: the ip_address shown on other proxy types is a one-time + observation, not a stable identity. + limit: Limit the number of proxies to return. name: Exact-match filter on proxy name using the database collation. In production, @@ -570,6 +609,7 @@ def list( timeout=timeout, query=maybe_transform( { + "ip_address": ip_address, "limit": limit, "name": name, "offset": offset, diff --git a/src/kernel/resources/proxy_ips.py b/src/kernel/resources/proxy_ips.py new file mode 100644 index 00000000..cd56d37b --- /dev/null +++ b/src/kernel/resources/proxy_ips.py @@ -0,0 +1,319 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import proxy_ip_list_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncOffsetPagination, AsyncOffsetPagination +from .._base_client import AsyncPaginator, make_request_options +from ..types.proxy_ip import ProxyIP +from ..types.proxy_ip_detail import ProxyIPDetail + +__all__ = ["ProxyIPsResource", "AsyncProxyIPsResource"] + + +class ProxyIPsResource(SyncAPIResource): + """Create and manage proxy configurations for routing browser traffic.""" + + @cached_property + def with_raw_response(self) -> ProxyIPsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return ProxyIPsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProxyIPsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return ProxyIPsResourceWithStreamingResponse(self) + + def retrieve( + self, + ip_address: 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, + ) -> ProxyIPDetail: + """ + Retrieve one reserved IP with the ISP proxies using it as their primary exit. + proxy_count spans the organization; proxies lists only those in the current + project. Returns 404 when the IP is not in the organization's allocation and 403 + with code reserved_isp_not_enabled when the organization has none. Scoped API + keys require proxies:read on the whole project or organization, not on + individual proxies; otherwise the operation returns 403 with code + insufficient_scope. + + 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 ip_address: + raise ValueError(f"Expected a non-empty value for `ip_address` but received {ip_address!r}") + return self._get( + path_template("/proxy_ips/{ip_address}", ip_address=ip_address), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyIPDetail, + ) + + def list( + self, + *, + country: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + status: Literal["active", "excluded"] | 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, + ) -> SyncOffsetPagination[ProxyIP]: + """ + List the IP addresses reserved for the organization's ISP proxies, with each + IP's country and whether it can currently be assigned. Available only to + organizations with a reserved ISP allocation; others receive a 403 with code + reserved_isp_not_enabled. Served from the allocation itself, so IPs appear + whether or not a proxy uses them. Scoped API keys require proxies:read on the + whole project or organization, not on individual proxies; otherwise the + operation returns 403 with code insufficient_scope. + + Args: + country: ISO 3166 country code to filter by. Must be a country ISP proxies support. + + limit: Limit the number of IPs to return. + + offset: Offset the number of IPs to return. + + status: Filter by whether the IP can currently be assigned. + + 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 + """ + return self._get_api_list( + "/proxy_ips", + page=SyncOffsetPagination[ProxyIP], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "country": country, + "limit": limit, + "offset": offset, + "status": status, + }, + proxy_ip_list_params.ProxyIPListParams, + ), + ), + model=ProxyIP, + ) + + +class AsyncProxyIPsResource(AsyncAPIResource): + """Create and manage proxy configurations for routing browser traffic.""" + + @cached_property + def with_raw_response(self) -> AsyncProxyIPsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncProxyIPsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProxyIPsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/kernel/kernel-python-sdk#with_streaming_response + """ + return AsyncProxyIPsResourceWithStreamingResponse(self) + + async def retrieve( + self, + ip_address: 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, + ) -> ProxyIPDetail: + """ + Retrieve one reserved IP with the ISP proxies using it as their primary exit. + proxy_count spans the organization; proxies lists only those in the current + project. Returns 404 when the IP is not in the organization's allocation and 403 + with code reserved_isp_not_enabled when the organization has none. Scoped API + keys require proxies:read on the whole project or organization, not on + individual proxies; otherwise the operation returns 403 with code + insufficient_scope. + + 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 ip_address: + raise ValueError(f"Expected a non-empty value for `ip_address` but received {ip_address!r}") + return await self._get( + path_template("/proxy_ips/{ip_address}", ip_address=ip_address), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProxyIPDetail, + ) + + def list( + self, + *, + country: str | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + status: Literal["active", "excluded"] | 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, + ) -> AsyncPaginator[ProxyIP, AsyncOffsetPagination[ProxyIP]]: + """ + List the IP addresses reserved for the organization's ISP proxies, with each + IP's country and whether it can currently be assigned. Available only to + organizations with a reserved ISP allocation; others receive a 403 with code + reserved_isp_not_enabled. Served from the allocation itself, so IPs appear + whether or not a proxy uses them. Scoped API keys require proxies:read on the + whole project or organization, not on individual proxies; otherwise the + operation returns 403 with code insufficient_scope. + + Args: + country: ISO 3166 country code to filter by. Must be a country ISP proxies support. + + limit: Limit the number of IPs to return. + + offset: Offset the number of IPs to return. + + status: Filter by whether the IP can currently be assigned. + + 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 + """ + return self._get_api_list( + "/proxy_ips", + page=AsyncOffsetPagination[ProxyIP], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "country": country, + "limit": limit, + "offset": offset, + "status": status, + }, + proxy_ip_list_params.ProxyIPListParams, + ), + ), + model=ProxyIP, + ) + + +class ProxyIPsResourceWithRawResponse: + def __init__(self, proxy_ips: ProxyIPsResource) -> None: + self._proxy_ips = proxy_ips + + self.retrieve = to_raw_response_wrapper( + proxy_ips.retrieve, + ) + self.list = to_raw_response_wrapper( + proxy_ips.list, + ) + + +class AsyncProxyIPsResourceWithRawResponse: + def __init__(self, proxy_ips: AsyncProxyIPsResource) -> None: + self._proxy_ips = proxy_ips + + self.retrieve = async_to_raw_response_wrapper( + proxy_ips.retrieve, + ) + self.list = async_to_raw_response_wrapper( + proxy_ips.list, + ) + + +class ProxyIPsResourceWithStreamingResponse: + def __init__(self, proxy_ips: ProxyIPsResource) -> None: + self._proxy_ips = proxy_ips + + self.retrieve = to_streamed_response_wrapper( + proxy_ips.retrieve, + ) + self.list = to_streamed_response_wrapper( + proxy_ips.list, + ) + + +class AsyncProxyIPsResourceWithStreamingResponse: + def __init__(self, proxy_ips: AsyncProxyIPsResource) -> None: + self._proxy_ips = proxy_ips + + self.retrieve = async_to_streamed_response_wrapper( + proxy_ips.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + proxy_ips.list, + ) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index eb5e4193..45cc0ec4 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -30,6 +30,7 @@ from .warning import Warning as Warning from .analysis import Analysis as Analysis from .evidence import Evidence as Evidence +from .proxy_ip import ProxyIP as ProxyIP from .credential import Credential as Credential from .tags_param import TagsParam as TagsParam from .browser_pool import BrowserPool as BrowserPool @@ -42,6 +43,7 @@ from .audit_log_entry import AuditLogEntry as AuditLogEntry from .created_api_key import CreatedAPIKey as CreatedAPIKey from .lookup_response import LookupResponse as LookupResponse +from .proxy_ip_detail import ProxyIPDetail as ProxyIPDetail from .vault_reference import VaultReference as VaultReference from .analysis_summary import AnalysisSummary as AnalysisSummary from .browser_location import BrowserLocation as BrowserLocation @@ -68,6 +70,7 @@ from .browser_repl_content import BrowserReplContent as BrowserReplContent from .browser_video_memory import BrowserVideoMemory as BrowserVideoMemory from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse +from .proxy_ip_list_params import ProxyIPListParams as ProxyIPListParams from .search_create_params import SearchCreateParams as SearchCreateParams from .vault_encryption_key import VaultEncryptionKey as VaultEncryptionKey from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams diff --git a/src/kernel/types/proxy.py b/src/kernel/types/proxy.py index af5045f9..773578f2 100644 --- a/src/kernel/types/proxy.py +++ b/src/kernel/types/proxy.py @@ -32,7 +32,19 @@ class ConfigRegistryManagedProxyCreateConfigIspProxyConfig(BaseModel): """ISO 3166 country code. Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not - provided. + provided, or to the country of ip_address when that is set. + """ + + ip_address: Optional[str] = None + """Reserved exit IP to pin the proxy to. + + Available only to organizations with a reserved ISP allocation; the IP must be + active in it (see GET /proxy_ips) and, when country is also set, in that + country. Omit to let Kernel choose an IP. A proxy read includes it only when the + proxy's primary exit IP is one of the organization's reserved IPs; every ISP + proxy's exit IP is on the top-level ip_address. An IP outside the allocation is + rejected with code ip_not_reserved; one that cannot currently be assigned, with + 409 ip_unavailable. """ diff --git a/src/kernel/types/proxy_check_response.py b/src/kernel/types/proxy_check_response.py index 75ee4655..14833e29 100644 --- a/src/kernel/types/proxy_check_response.py +++ b/src/kernel/types/proxy_check_response.py @@ -23,7 +23,19 @@ class ConfigIspProxyConfig(BaseModel): """ISO 3166 country code. Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not - provided. + provided, or to the country of ip_address when that is set. + """ + + ip_address: Optional[str] = None + """Reserved exit IP to pin the proxy to. + + Available only to organizations with a reserved ISP allocation; the IP must be + active in it (see GET /proxy_ips) and, when country is also set, in that + country. Omit to let Kernel choose an IP. A proxy read includes it only when the + proxy's primary exit IP is one of the organization's reserved IPs; every ISP + proxy's exit IP is on the top-level ip_address. An IP outside the allocation is + rejected with code ip_not_reserved; one that cannot currently be assigned, with + 409 ip_unavailable. """ diff --git a/src/kernel/types/proxy_create_params.py b/src/kernel/types/proxy_create_params.py index 6872e9fb..22207be5 100644 --- a/src/kernel/types/proxy_create_params.py +++ b/src/kernel/types/proxy_create_params.py @@ -40,7 +40,19 @@ class ConfigIspProxyConfig(TypedDict, total=False): """ISO 3166 country code. Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not - provided. + provided, or to the country of ip_address when that is set. + """ + + ip_address: str + """Reserved exit IP to pin the proxy to. + + Available only to organizations with a reserved ISP allocation; the IP must be + active in it (see GET /proxy_ips) and, when country is also set, in that + country. Omit to let Kernel choose an IP. A proxy read includes it only when the + proxy's primary exit IP is one of the organization's reserved IPs; every ISP + proxy's exit IP is on the top-level ip_address. An IP outside the allocation is + rejected with code ip_not_reserved; one that cannot currently be assigned, with + 409 ip_unavailable. """ diff --git a/src/kernel/types/proxy_create_response.py b/src/kernel/types/proxy_create_response.py index f4d96af8..57551507 100644 --- a/src/kernel/types/proxy_create_response.py +++ b/src/kernel/types/proxy_create_response.py @@ -23,7 +23,19 @@ class ConfigIspProxyConfig(BaseModel): """ISO 3166 country code. Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not - provided. + provided, or to the country of ip_address when that is set. + """ + + ip_address: Optional[str] = None + """Reserved exit IP to pin the proxy to. + + Available only to organizations with a reserved ISP allocation; the IP must be + active in it (see GET /proxy_ips) and, when country is also set, in that + country. Omit to let Kernel choose an IP. A proxy read includes it only when the + proxy's primary exit IP is one of the organization's reserved IPs; every ISP + proxy's exit IP is on the top-level ip_address. An IP outside the allocation is + rejected with code ip_not_reserved; one that cannot currently be assigned, with + 409 ip_unavailable. """ diff --git a/src/kernel/types/proxy_ip.py b/src/kernel/types/proxy_ip.py new file mode 100644 index 00000000..789f058e --- /dev/null +++ b/src/kernel/types/proxy_ip.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["ProxyIP"] + + +class ProxyIP(BaseModel): + """One IP address reserved for the organization's ISP proxies.""" + + country: str + """ISO 3166 country code of the IP.""" + + ip_address: str + """Reserved exit IP address.""" + + status: Literal["active", "excluded"] + """Whether the IP can currently be assigned. + + An excluded IP is listed but cannot be pinned or moved to; proxies already on it + keep working. + """ diff --git a/src/kernel/types/proxy_ip_detail.py b/src/kernel/types/proxy_ip_detail.py new file mode 100644 index 00000000..158f2699 --- /dev/null +++ b/src/kernel/types/proxy_ip_detail.py @@ -0,0 +1,45 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["ProxyIPDetail", "Proxy"] + + +class Proxy(BaseModel): + """An ISP proxy whose primary exit is the reserved IP.""" + + id: str + + name: Optional[str] = None + + +class ProxyIPDetail(BaseModel): + """A reserved IP with the ISP proxies using it as their primary exit. + + Reservations are organization-wide, so proxy_count covers every project while proxies lists only those in the current project. + """ + + country: str + """ISO 3166 country code of the IP.""" + + ip_address: str + """Reserved exit IP address.""" + + proxies: List[Proxy] + """ISP proxies in the current project whose primary exit IP is this address.""" + + proxy_count: int + """ + Number of ISP proxies across the organization whose primary exit IP is this + address. + """ + + status: Literal["active", "excluded"] + """Whether the IP can currently be assigned. + + An excluded IP is listed but cannot be pinned or moved to; proxies already on it + keep working. + """ diff --git a/src/kernel/types/proxy_ip_list_params.py b/src/kernel/types/proxy_ip_list_params.py new file mode 100644 index 00000000..91ef586b --- /dev/null +++ b/src/kernel/types/proxy_ip_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ProxyIPListParams"] + + +class ProxyIPListParams(TypedDict, total=False): + country: str + """ISO 3166 country code to filter by. Must be a country ISP proxies support.""" + + limit: int + """Limit the number of IPs to return.""" + + offset: int + """Offset the number of IPs to return.""" + + status: Literal["active", "excluded"] + """Filter by whether the IP can currently be assigned.""" diff --git a/src/kernel/types/proxy_list_params.py b/src/kernel/types/proxy_list_params.py index 1c7b30e6..e0ddbc07 100644 --- a/src/kernel/types/proxy_list_params.py +++ b/src/kernel/types/proxy_list_params.py @@ -8,6 +8,13 @@ class ProxyListParams(TypedDict, total=False): + ip_address: str + """Exact match on the primary exit IP of ISP proxies. + + Must be an IPv4 address. Only ISP proxies are matched: the ip_address shown on + other proxy types is a one-time observation, not a stable identity. + """ + limit: int """Limit the number of proxies to return.""" diff --git a/src/kernel/types/proxy_list_response.py b/src/kernel/types/proxy_list_response.py index 82d1bd18..53287e19 100644 --- a/src/kernel/types/proxy_list_response.py +++ b/src/kernel/types/proxy_list_response.py @@ -23,7 +23,19 @@ class ConfigIspProxyConfig(BaseModel): """ISO 3166 country code. Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not - provided. + provided, or to the country of ip_address when that is set. + """ + + ip_address: Optional[str] = None + """Reserved exit IP to pin the proxy to. + + Available only to organizations with a reserved ISP allocation; the IP must be + active in it (see GET /proxy_ips) and, when country is also set, in that + country. Omit to let Kernel choose an IP. A proxy read includes it only when the + proxy's primary exit IP is one of the organization's reserved IPs; every ISP + proxy's exit IP is on the top-level ip_address. An IP outside the allocation is + rejected with code ip_not_reserved; one that cannot currently be assigned, with + 409 ip_unavailable. """ diff --git a/src/kernel/types/proxy_retrieve_response.py b/src/kernel/types/proxy_retrieve_response.py index c59de78a..5d04f83a 100644 --- a/src/kernel/types/proxy_retrieve_response.py +++ b/src/kernel/types/proxy_retrieve_response.py @@ -23,7 +23,19 @@ class ConfigIspProxyConfig(BaseModel): """ISO 3166 country code. Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not - provided. + provided, or to the country of ip_address when that is set. + """ + + ip_address: Optional[str] = None + """Reserved exit IP to pin the proxy to. + + Available only to organizations with a reserved ISP allocation; the IP must be + active in it (see GET /proxy_ips) and, when country is also set, in that + country. Omit to let Kernel choose an IP. A proxy read includes it only when the + proxy's primary exit IP is one of the organization's reserved IPs; every ISP + proxy's exit IP is on the top-level ip_address. An IP outside the allocation is + rejected with code ip_not_reserved; one that cannot currently be assigned, with + 409 ip_unavailable. """ diff --git a/src/kernel/types/proxy_update_params.py b/src/kernel/types/proxy_update_params.py index 0db118a4..2aa9ab16 100644 --- a/src/kernel/types/proxy_update_params.py +++ b/src/kernel/types/proxy_update_params.py @@ -4,13 +4,30 @@ from typing_extensions import Required, TypedDict -__all__ = ["ProxyUpdateParams"] +__all__ = ["ProxyUpdateParams", "Config"] class ProxyUpdateParams(TypedDict, total=False): - name: Required[str] + config: Config + """Changes to an ISP proxy's exit IP. Accepted only for proxies of type `isp`.""" + + name: str """New proxy name. Proxy names are trimmed and length-checked only; duplicates are allowed because proxies are updated by ID, not by name. """ + + +class Config(TypedDict, total=False): + """Changes to an ISP proxy's exit IP. Accepted only for proxies of type `isp`.""" + + ip_address: Required[str] + """Reserved exit IP to move the proxy to. + + The IP must be active in the organization's allocation and in the proxy's + country. The proxy keeps its ID. The new endpoint is probed before anything + changes; a failed probe leaves the proxy as it was. Live browsers open new + connections on the new IP within a few seconds; established connections drain on + the old one. + """ diff --git a/src/kernel/types/proxy_update_response.py b/src/kernel/types/proxy_update_response.py index 323a3731..f87186bc 100644 --- a/src/kernel/types/proxy_update_response.py +++ b/src/kernel/types/proxy_update_response.py @@ -23,7 +23,19 @@ class ConfigIspProxyConfig(BaseModel): """ISO 3166 country code. Supported countries are US, GB, FR, DE, SG, and KR. Defaults to US if not - provided. + provided, or to the country of ip_address when that is set. + """ + + ip_address: Optional[str] = None + """Reserved exit IP to pin the proxy to. + + Available only to organizations with a reserved ISP allocation; the IP must be + active in it (see GET /proxy_ips) and, when country is also set, in that + country. Omit to let Kernel choose an IP. A proxy read includes it only when the + proxy's primary exit IP is one of the organization's reserved IPs; every ISP + proxy's exit IP is on the top-level ip_address. An IP outside the allocation is + rejected with code ip_not_reserved; one that cannot currently be assigned, with + 409 ip_unavailable. """ diff --git a/tests/api_resources/test_proxies.py b/tests/api_resources/test_proxies.py index a8737804..aa01e3cf 100644 --- a/tests/api_resources/test_proxies.py +++ b/tests/api_resources/test_proxies.py @@ -38,7 +38,10 @@ def test_method_create_with_all_params(self, client: Kernel) -> None: proxy = client.proxies.create( type="isp", bypass_hosts=["string"], - config={"country": "US"}, + config={ + "country": "US", + "ip_address": "203.0.113.10", + }, name="name", protocol="http", ) @@ -117,6 +120,15 @@ def test_path_params_retrieve(self, client: Kernel) -> None: def test_method_update(self, client: Kernel) -> None: proxy = client.proxies.update( id_or_name="id_or_name", + ) + assert_matches_type(ProxyUpdateResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + proxy = client.proxies.update( + id_or_name="id_or_name", + config={"ip_address": "203.0.113.10"}, name="my-renamed-proxy", ) assert_matches_type(ProxyUpdateResponse, proxy, path=["response"]) @@ -126,7 +138,6 @@ def test_method_update(self, client: Kernel) -> None: def test_raw_response_update(self, client: Kernel) -> None: response = client.proxies.with_raw_response.update( id_or_name="id_or_name", - name="my-renamed-proxy", ) assert response.is_closed is True @@ -139,7 +150,6 @@ def test_raw_response_update(self, client: Kernel) -> None: def test_streaming_response_update(self, client: Kernel) -> None: with client.proxies.with_streaming_response.update( id_or_name="id_or_name", - name="my-renamed-proxy", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -155,7 +165,6 @@ def test_path_params_update(self, client: Kernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): client.proxies.with_raw_response.update( id_or_name="", - name="my-renamed-proxy", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -168,6 +177,7 @@ def test_method_list(self, client: Kernel) -> None: @parametrize def test_method_list_with_all_params(self, client: Kernel) -> None: proxy = client.proxies.list( + ip_address="ip_address", limit=1, name="name", offset=0, @@ -310,7 +320,10 @@ async def test_method_create_with_all_params(self, async_client: AsyncKernel) -> proxy = await async_client.proxies.create( type="isp", bypass_hosts=["string"], - config={"country": "US"}, + config={ + "country": "US", + "ip_address": "203.0.113.10", + }, name="name", protocol="http", ) @@ -389,6 +402,15 @@ async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: async def test_method_update(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.update( id_or_name="id_or_name", + ) + assert_matches_type(ProxyUpdateResponse, proxy, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncKernel) -> None: + proxy = await async_client.proxies.update( + id_or_name="id_or_name", + config={"ip_address": "203.0.113.10"}, name="my-renamed-proxy", ) assert_matches_type(ProxyUpdateResponse, proxy, path=["response"]) @@ -398,7 +420,6 @@ async def test_method_update(self, async_client: AsyncKernel) -> None: async def test_raw_response_update(self, async_client: AsyncKernel) -> None: response = await async_client.proxies.with_raw_response.update( id_or_name="id_or_name", - name="my-renamed-proxy", ) assert response.is_closed is True @@ -411,7 +432,6 @@ async def test_raw_response_update(self, async_client: AsyncKernel) -> None: async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: async with async_client.proxies.with_streaming_response.update( id_or_name="id_or_name", - name="my-renamed-proxy", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -427,7 +447,6 @@ async def test_path_params_update(self, async_client: AsyncKernel) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"): await async_client.proxies.with_raw_response.update( id_or_name="", - name="my-renamed-proxy", ) @pytest.mark.skip(reason="Mock server tests are disabled") @@ -440,6 +459,7 @@ async def test_method_list(self, async_client: AsyncKernel) -> None: @parametrize async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: proxy = await async_client.proxies.list( + ip_address="ip_address", limit=1, name="name", offset=0, diff --git a/tests/api_resources/test_proxy_ips.py b/tests/api_resources/test_proxy_ips.py new file mode 100644 index 00000000..49d8e7f6 --- /dev/null +++ b/tests/api_resources/test_proxy_ips.py @@ -0,0 +1,187 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from kernel import Kernel, AsyncKernel +from tests.utils import assert_matches_type +from kernel.types import ProxyIP, ProxyIPDetail +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestProxyIPs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + proxy_ip = client.proxy_ips.retrieve( + "ip_address", + ) + assert_matches_type(ProxyIPDetail, proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.proxy_ips.with_raw_response.retrieve( + "ip_address", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy_ip = response.parse() + assert_matches_type(ProxyIPDetail, proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.proxy_ips.with_streaming_response.retrieve( + "ip_address", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy_ip = response.parse() + assert_matches_type(ProxyIPDetail, proxy_ip, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `ip_address` but received ''"): + client.proxy_ips.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + proxy_ip = client.proxy_ips.list() + assert_matches_type(SyncOffsetPagination[ProxyIP], proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + proxy_ip = client.proxy_ips.list( + country="country", + limit=1, + offset=0, + status="active", + ) + assert_matches_type(SyncOffsetPagination[ProxyIP], proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.proxy_ips.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy_ip = response.parse() + assert_matches_type(SyncOffsetPagination[ProxyIP], proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.proxy_ips.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy_ip = response.parse() + assert_matches_type(SyncOffsetPagination[ProxyIP], proxy_ip, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncProxyIPs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + proxy_ip = await async_client.proxy_ips.retrieve( + "ip_address", + ) + assert_matches_type(ProxyIPDetail, proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncKernel) -> None: + response = await async_client.proxy_ips.with_raw_response.retrieve( + "ip_address", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy_ip = await response.parse() + assert_matches_type(ProxyIPDetail, proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncKernel) -> None: + async with async_client.proxy_ips.with_streaming_response.retrieve( + "ip_address", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy_ip = await response.parse() + assert_matches_type(ProxyIPDetail, proxy_ip, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `ip_address` but received ''"): + await async_client.proxy_ips.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + proxy_ip = await async_client.proxy_ips.list() + assert_matches_type(AsyncOffsetPagination[ProxyIP], proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncKernel) -> None: + proxy_ip = await async_client.proxy_ips.list( + country="country", + limit=1, + offset=0, + status="active", + ) + assert_matches_type(AsyncOffsetPagination[ProxyIP], proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncKernel) -> None: + response = await async_client.proxy_ips.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + proxy_ip = await response.parse() + assert_matches_type(AsyncOffsetPagination[ProxyIP], proxy_ip, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncKernel) -> None: + async with async_client.proxy_ips.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + proxy_ip = await response.parse() + assert_matches_type(AsyncOffsetPagination[ProxyIP], proxy_ip, path=["response"]) + + assert cast(Any, response.is_closed) is True From 65c930fdec7023f9ee0c59f4690412202e2bbc5d Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:22:40 +0000 Subject: [PATCH 3/3] release: 0.142.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- src/kernel/_version.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ab58c193..9ad4bf82 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.141.0" + ".": "0.142.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a13734..fbb0e603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.142.0](https://github.com/kernel/kernel-python-sdk/compare/v0.141.0...v0.142.0) (2026-11-06) + + +### Features + +* Let reserved orgs list, pin and move ISP proxy IPs ([cfda76a](https://github.com/kernel/kernel-python-sdk/commit/cfda76aba98f670654ecb40853a33e8bfaeeb82e)) + ## [0.141.0](https://github.com/kernel/kernel-python-sdk/compare/v0.140.0...v0.141.0) (2026-11-05) diff --git a/pyproject.toml b/pyproject.toml index ac6d9740..800a270e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.141.0" +version = "0.142.0" description = "The official Python library for the kernel API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 8483c62b..efd81788 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.141.0" # x-release-please-version +__version__ = "0.142.0" # x-release-please-version