diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 141e7cd..ff66120 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.47.0" + ".": "0.48.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 7f48513..db7b03c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 104 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-ac10847d991ef8ed89124b5550922cb5726af2b4a4c3396ee6ff82938302fc25.yml -openapi_spec_hash: 0d902563108fe2461708c05336eab40a -config_hash: 16e4457a0bb26e98a335a1c2a572290a +configured_endpoints: 111 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-49a1a92e00d1eb87e91e8527275cb0705fce2edea30e70fea745f134dd451fbd.yml +openapi_spec_hash: 3aa6ab6939790f538332054162fbdedc +config_hash: 9818dd634f87b677410eefd013d7a179 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e99ff9..3e4f398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.48.0 (2026-04-10) + +Full Changelog: [v0.47.0...v0.48.0](https://github.com/kernel/kernel-python-sdk/compare/v0.47.0...v0.48.0) + +### Features + +* [kernel-1116] add base_url field to browser session response ([335d9e0](https://github.com/kernel/kernel-python-sdk/commit/335d9e04998dd9e581f47d8869dd7f07a8f2db74)) + + +### Bug Fixes + +* **client:** preserve hardcoded query params when merging with user params ([6dfd882](https://github.com/kernel/kernel-python-sdk/commit/6dfd8826b84ed191d72ac114df986c398fa83c5c)) + + +### Chores + +* retrigger Stainless codegen for projects resource ([ca22fd9](https://github.com/kernel/kernel-python-sdk/commit/ca22fd9dc4b5f4cd70e15ecb1ddd3607d8a99df8)) + ## 0.47.0 (2026-04-07) Full Changelog: [v0.46.0...v0.47.0](https://github.com/kernel/kernel-python-sdk/compare/v0.46.0...v0.47.0) diff --git a/api.md b/api.md index 5e43066..96c90c4 100644 --- a/api.md +++ b/api.md @@ -341,6 +341,35 @@ Methods: - client.credentials.delete(id_or_name) -> None - client.credentials.totp_code(id_or_name) -> CredentialTotpCodeResponse +# Projects + +Types: + +```python +from kernel.types import CreateProjectRequest, Project, UpdateProjectRequest +``` + +Methods: + +- client.projects.create(\*\*params) -> Project +- client.projects.retrieve(id) -> Project +- client.projects.update(id, \*\*params) -> Project +- client.projects.list(\*\*params) -> SyncOffsetPagination[Project] +- client.projects.delete(id) -> None + +## Limits + +Types: + +```python +from kernel.types.projects import ProjectLimits, UpdateProjectLimitsRequest +``` + +Methods: + +- client.projects.limits.retrieve(id) -> ProjectLimits +- client.projects.limits.update(id, \*\*params) -> ProjectLimits + # CredentialProviders Types: diff --git a/pyproject.toml b/pyproject.toml index c9fe0cd..b26c2b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kernel" -version = "0.47.0" +version = "0.48.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 a3d47ea..2599dc4 100644 --- a/src/kernel/_base_client.py +++ b/src/kernel/_base_client.py @@ -540,6 +540,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} diff --git a/src/kernel/_client.py b/src/kernel/_client.py index 84fc48d..75fe4b6 100644 --- a/src/kernel/_client.py +++ b/src/kernel/_client.py @@ -37,6 +37,7 @@ proxies, browsers, profiles, + projects, extensions, credentials, deployments, @@ -54,6 +55,7 @@ from .resources.invocations import InvocationsResource, AsyncInvocationsResource from .resources.browser_pools import BrowserPoolsResource, AsyncBrowserPoolsResource from .resources.browsers.browsers import BrowsersResource, AsyncBrowsersResource + from .resources.projects.projects import ProjectsResource, AsyncProjectsResource from .resources.credential_providers import CredentialProvidersResource, AsyncCredentialProvidersResource __all__ = [ @@ -222,6 +224,13 @@ def credentials(self) -> CredentialsResource: return CredentialsResource(self) + @cached_property + def projects(self) -> ProjectsResource: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import ProjectsResource + + return ProjectsResource(self) + @cached_property def credential_providers(self) -> CredentialProvidersResource: """Configure external credential providers like 1Password.""" @@ -492,6 +501,13 @@ def credentials(self) -> AsyncCredentialsResource: return AsyncCredentialsResource(self) + @cached_property + def projects(self) -> AsyncProjectsResource: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import AsyncProjectsResource + + return AsyncProjectsResource(self) + @cached_property def credential_providers(self) -> AsyncCredentialProvidersResource: """Configure external credential providers like 1Password.""" @@ -689,6 +705,13 @@ def credentials(self) -> credentials.CredentialsResourceWithRawResponse: return CredentialsResourceWithRawResponse(self._client.credentials) + @cached_property + def projects(self) -> projects.ProjectsResourceWithRawResponse: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import ProjectsResourceWithRawResponse + + return ProjectsResourceWithRawResponse(self._client.projects) + @cached_property def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithRawResponse: """Configure external credential providers like 1Password.""" @@ -772,6 +795,13 @@ def credentials(self) -> credentials.AsyncCredentialsResourceWithRawResponse: return AsyncCredentialsResourceWithRawResponse(self._client.credentials) + @cached_property + def projects(self) -> projects.AsyncProjectsResourceWithRawResponse: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import AsyncProjectsResourceWithRawResponse + + return AsyncProjectsResourceWithRawResponse(self._client.projects) + @cached_property def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithRawResponse: """Configure external credential providers like 1Password.""" @@ -855,6 +885,13 @@ def credentials(self) -> credentials.CredentialsResourceWithStreamingResponse: return CredentialsResourceWithStreamingResponse(self._client.credentials) + @cached_property + def projects(self) -> projects.ProjectsResourceWithStreamingResponse: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import ProjectsResourceWithStreamingResponse + + return ProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property def credential_providers(self) -> credential_providers.CredentialProvidersResourceWithStreamingResponse: """Configure external credential providers like 1Password.""" @@ -938,6 +975,13 @@ def credentials(self) -> credentials.AsyncCredentialsResourceWithStreamingRespon return AsyncCredentialsResourceWithStreamingResponse(self._client.credentials) + @cached_property + def projects(self) -> projects.AsyncProjectsResourceWithStreamingResponse: + """Create and manage projects for resource isolation within an organization.""" + from .resources.projects import AsyncProjectsResourceWithStreamingResponse + + return AsyncProjectsResourceWithStreamingResponse(self._client.projects) + @cached_property def credential_providers(self) -> credential_providers.AsyncCredentialProvidersResourceWithStreamingResponse: """Configure external credential providers like 1Password.""" diff --git a/src/kernel/_version.py b/src/kernel/_version.py index 254e7e2..a9881de 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.47.0" # x-release-please-version +__version__ = "0.48.0" # x-release-please-version diff --git a/src/kernel/resources/__init__.py b/src/kernel/resources/__init__.py index 4896e79..f078a03 100644 --- a/src/kernel/resources/__init__.py +++ b/src/kernel/resources/__init__.py @@ -40,6 +40,14 @@ ProfilesResourceWithStreamingResponse, AsyncProfilesResourceWithStreamingResponse, ) +from .projects import ( + ProjectsResource, + AsyncProjectsResource, + ProjectsResourceWithRawResponse, + AsyncProjectsResourceWithRawResponse, + ProjectsResourceWithStreamingResponse, + AsyncProjectsResourceWithStreamingResponse, +) from .extensions import ( ExtensionsResource, AsyncExtensionsResource, @@ -150,6 +158,12 @@ "AsyncCredentialsResourceWithRawResponse", "CredentialsResourceWithStreamingResponse", "AsyncCredentialsResourceWithStreamingResponse", + "ProjectsResource", + "AsyncProjectsResource", + "ProjectsResourceWithRawResponse", + "AsyncProjectsResourceWithRawResponse", + "ProjectsResourceWithStreamingResponse", + "AsyncProjectsResourceWithStreamingResponse", "CredentialProvidersResource", "AsyncCredentialProvidersResource", "CredentialProvidersResourceWithRawResponse", diff --git a/src/kernel/resources/projects/__init__.py b/src/kernel/resources/projects/__init__.py new file mode 100644 index 0000000..4126389 --- /dev/null +++ b/src/kernel/resources/projects/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .limits import ( + LimitsResource, + AsyncLimitsResource, + LimitsResourceWithRawResponse, + AsyncLimitsResourceWithRawResponse, + LimitsResourceWithStreamingResponse, + AsyncLimitsResourceWithStreamingResponse, +) +from .projects import ( + ProjectsResource, + AsyncProjectsResource, + ProjectsResourceWithRawResponse, + AsyncProjectsResourceWithRawResponse, + ProjectsResourceWithStreamingResponse, + AsyncProjectsResourceWithStreamingResponse, +) + +__all__ = [ + "LimitsResource", + "AsyncLimitsResource", + "LimitsResourceWithRawResponse", + "AsyncLimitsResourceWithRawResponse", + "LimitsResourceWithStreamingResponse", + "AsyncLimitsResourceWithStreamingResponse", + "ProjectsResource", + "AsyncProjectsResource", + "ProjectsResourceWithRawResponse", + "AsyncProjectsResourceWithRawResponse", + "ProjectsResourceWithStreamingResponse", + "AsyncProjectsResourceWithStreamingResponse", +] diff --git a/src/kernel/resources/projects/limits.py b/src/kernel/resources/projects/limits.py new file mode 100644 index 0000000..eeff593 --- /dev/null +++ b/src/kernel/resources/projects/limits.py @@ -0,0 +1,309 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_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 ..._base_client import make_request_options +from ...types.projects import limit_update_params +from ...types.projects.project_limits import ProjectLimits + +__all__ = ["LimitsResource", "AsyncLimitsResource"] + + +class LimitsResource(SyncAPIResource): + """Create and manage projects for resource isolation within an organization.""" + + @cached_property + def with_raw_response(self) -> LimitsResourceWithRawResponse: + """ + 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 LimitsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LimitsResourceWithStreamingResponse: + """ + 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 LimitsResourceWithStreamingResponse(self) + + def retrieve( + 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, + ) -> ProjectLimits: + """Get the resource limit overrides for a project. + + Null values mean no + project-level cap (org limit applies). + + 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( + path_template("/projects/{id}/limits", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectLimits, + ) + + def update( + self, + id: str, + *, + max_concurrent_invocations: Optional[int] | Omit = omit, + max_concurrent_sessions: Optional[int] | Omit = omit, + max_persistent_sessions: Optional[int] | Omit = omit, + max_pooled_sessions: Optional[int] | 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, + ) -> ProjectLimits: + """Update resource limit overrides for a project. + + Only fields present in the + request are modified. Set a field to 0 to remove that limit cap; omit a field to + leave it unchanged. + + Args: + max_concurrent_invocations: Maximum concurrent app invocations for this project. Set to 0 to remove the cap; + omit to leave unchanged. + + max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the + cap; omit to leave unchanged. + + max_persistent_sessions: Maximum persistent browser sessions for this project. Set to 0 to remove the + cap; omit to leave unchanged. + + max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap; + omit to leave unchanged. + + 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._patch( + path_template("/projects/{id}/limits", id=id), + body=maybe_transform( + { + "max_concurrent_invocations": max_concurrent_invocations, + "max_concurrent_sessions": max_concurrent_sessions, + "max_persistent_sessions": max_persistent_sessions, + "max_pooled_sessions": max_pooled_sessions, + }, + limit_update_params.LimitUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectLimits, + ) + + +class AsyncLimitsResource(AsyncAPIResource): + """Create and manage projects for resource isolation within an organization.""" + + @cached_property + def with_raw_response(self) -> AsyncLimitsResourceWithRawResponse: + """ + 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 AsyncLimitsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLimitsResourceWithStreamingResponse: + """ + 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 AsyncLimitsResourceWithStreamingResponse(self) + + async def retrieve( + 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, + ) -> ProjectLimits: + """Get the resource limit overrides for a project. + + Null values mean no + project-level cap (org limit applies). + + 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( + path_template("/projects/{id}/limits", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectLimits, + ) + + async def update( + self, + id: str, + *, + max_concurrent_invocations: Optional[int] | Omit = omit, + max_concurrent_sessions: Optional[int] | Omit = omit, + max_persistent_sessions: Optional[int] | Omit = omit, + max_pooled_sessions: Optional[int] | 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, + ) -> ProjectLimits: + """Update resource limit overrides for a project. + + Only fields present in the + request are modified. Set a field to 0 to remove that limit cap; omit a field to + leave it unchanged. + + Args: + max_concurrent_invocations: Maximum concurrent app invocations for this project. Set to 0 to remove the cap; + omit to leave unchanged. + + max_concurrent_sessions: Maximum concurrent browser sessions for this project. Set to 0 to remove the + cap; omit to leave unchanged. + + max_persistent_sessions: Maximum persistent browser sessions for this project. Set to 0 to remove the + cap; omit to leave unchanged. + + max_pooled_sessions: Maximum pooled sessions capacity for this project. Set to 0 to remove the cap; + omit to leave unchanged. + + 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._patch( + path_template("/projects/{id}/limits", id=id), + body=await async_maybe_transform( + { + "max_concurrent_invocations": max_concurrent_invocations, + "max_concurrent_sessions": max_concurrent_sessions, + "max_persistent_sessions": max_persistent_sessions, + "max_pooled_sessions": max_pooled_sessions, + }, + limit_update_params.LimitUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ProjectLimits, + ) + + +class LimitsResourceWithRawResponse: + def __init__(self, limits: LimitsResource) -> None: + self._limits = limits + + self.retrieve = to_raw_response_wrapper( + limits.retrieve, + ) + self.update = to_raw_response_wrapper( + limits.update, + ) + + +class AsyncLimitsResourceWithRawResponse: + def __init__(self, limits: AsyncLimitsResource) -> None: + self._limits = limits + + self.retrieve = async_to_raw_response_wrapper( + limits.retrieve, + ) + self.update = async_to_raw_response_wrapper( + limits.update, + ) + + +class LimitsResourceWithStreamingResponse: + def __init__(self, limits: LimitsResource) -> None: + self._limits = limits + + self.retrieve = to_streamed_response_wrapper( + limits.retrieve, + ) + self.update = to_streamed_response_wrapper( + limits.update, + ) + + +class AsyncLimitsResourceWithStreamingResponse: + def __init__(self, limits: AsyncLimitsResource) -> None: + self._limits = limits + + self.retrieve = async_to_streamed_response_wrapper( + limits.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + limits.update, + ) diff --git a/src/kernel/resources/projects/projects.py b/src/kernel/resources/projects/projects.py new file mode 100644 index 0000000..b40dc02 --- /dev/null +++ b/src/kernel/resources/projects/projects.py @@ -0,0 +1,586 @@ +# 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 .limits import ( + LimitsResource, + AsyncLimitsResource, + LimitsResourceWithRawResponse, + AsyncLimitsResourceWithRawResponse, + LimitsResourceWithStreamingResponse, + AsyncLimitsResourceWithStreamingResponse, +) +from ...types import project_list_params, project_create_params, project_update_params +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_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.project import Project + +__all__ = ["ProjectsResource", "AsyncProjectsResource"] + + +class ProjectsResource(SyncAPIResource): + """Create and manage projects for resource isolation within an organization.""" + + @cached_property + def limits(self) -> LimitsResource: + """Create and manage projects for resource isolation within an organization.""" + return LimitsResource(self._client) + + @cached_property + def with_raw_response(self) -> ProjectsResourceWithRawResponse: + """ + 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 ProjectsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProjectsResourceWithStreamingResponse: + """ + 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 ProjectsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: 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, + ) -> Project: + """Create a new project within the authenticated organization. + + Requires a paid plan + and the projects feature flag. + + Args: + name: Project name (1-255 characters) + + 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._post( + "/projects", + body=maybe_transform({"name": name}, project_create_params.ProjectCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def retrieve( + 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, + ) -> Project: + """ + Get a project 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( + path_template("/projects/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def update( + self, + id: str, + *, + name: str | Omit = omit, + status: Literal["active", "archived"] | 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, + ) -> Project: + """ + Update a project's name or status. + + Args: + name: New project name + + status: New project status + + 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._patch( + path_template("/projects/{id}", id=id), + body=maybe_transform( + { + "name": name, + "status": status, + }, + project_update_params.ProjectUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | 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[Project]: + """ + List projects for the authenticated organization. + + Args: + limit: Maximum number of results to return + + offset: Number of results to skip + + 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( + "/projects", + page=SyncOffsetPagination[Project], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + project_list_params.ProjectListParams, + ), + ), + model=Project, + ) + + def delete( + 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, + ) -> None: + """Soft-delete a project. + + The project must be empty (no active resources). + + 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}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + path_template("/projects/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncProjectsResource(AsyncAPIResource): + """Create and manage projects for resource isolation within an organization.""" + + @cached_property + def limits(self) -> AsyncLimitsResource: + """Create and manage projects for resource isolation within an organization.""" + return AsyncLimitsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncProjectsResourceWithRawResponse: + """ + 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 AsyncProjectsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProjectsResourceWithStreamingResponse: + """ + 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 AsyncProjectsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: 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, + ) -> Project: + """Create a new project within the authenticated organization. + + Requires a paid plan + and the projects feature flag. + + Args: + name: Project name (1-255 characters) + + 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 await self._post( + "/projects", + body=await async_maybe_transform({"name": name}, project_create_params.ProjectCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + async def retrieve( + 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, + ) -> Project: + """ + Get a project 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( + path_template("/projects/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + async def update( + self, + id: str, + *, + name: str | Omit = omit, + status: Literal["active", "archived"] | 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, + ) -> Project: + """ + Update a project's name or status. + + Args: + name: New project name + + status: New project status + + 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._patch( + path_template("/projects/{id}", id=id), + body=await async_maybe_transform( + { + "name": name, + "status": status, + }, + project_update_params.ProjectUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Project, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | 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[Project, AsyncOffsetPagination[Project]]: + """ + List projects for the authenticated organization. + + Args: + limit: Maximum number of results to return + + offset: Number of results to skip + + 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( + "/projects", + page=AsyncOffsetPagination[Project], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + project_list_params.ProjectListParams, + ), + ), + model=Project, + ) + + async def delete( + 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, + ) -> None: + """Soft-delete a project. + + The project must be empty (no active resources). + + 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}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + path_template("/projects/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class ProjectsResourceWithRawResponse: + def __init__(self, projects: ProjectsResource) -> None: + self._projects = projects + + self.create = to_raw_response_wrapper( + projects.create, + ) + self.retrieve = to_raw_response_wrapper( + projects.retrieve, + ) + self.update = to_raw_response_wrapper( + projects.update, + ) + self.list = to_raw_response_wrapper( + projects.list, + ) + self.delete = to_raw_response_wrapper( + projects.delete, + ) + + @cached_property + def limits(self) -> LimitsResourceWithRawResponse: + """Create and manage projects for resource isolation within an organization.""" + return LimitsResourceWithRawResponse(self._projects.limits) + + +class AsyncProjectsResourceWithRawResponse: + def __init__(self, projects: AsyncProjectsResource) -> None: + self._projects = projects + + self.create = async_to_raw_response_wrapper( + projects.create, + ) + self.retrieve = async_to_raw_response_wrapper( + projects.retrieve, + ) + self.update = async_to_raw_response_wrapper( + projects.update, + ) + self.list = async_to_raw_response_wrapper( + projects.list, + ) + self.delete = async_to_raw_response_wrapper( + projects.delete, + ) + + @cached_property + def limits(self) -> AsyncLimitsResourceWithRawResponse: + """Create and manage projects for resource isolation within an organization.""" + return AsyncLimitsResourceWithRawResponse(self._projects.limits) + + +class ProjectsResourceWithStreamingResponse: + def __init__(self, projects: ProjectsResource) -> None: + self._projects = projects + + self.create = to_streamed_response_wrapper( + projects.create, + ) + self.retrieve = to_streamed_response_wrapper( + projects.retrieve, + ) + self.update = to_streamed_response_wrapper( + projects.update, + ) + self.list = to_streamed_response_wrapper( + projects.list, + ) + self.delete = to_streamed_response_wrapper( + projects.delete, + ) + + @cached_property + def limits(self) -> LimitsResourceWithStreamingResponse: + """Create and manage projects for resource isolation within an organization.""" + return LimitsResourceWithStreamingResponse(self._projects.limits) + + +class AsyncProjectsResourceWithStreamingResponse: + def __init__(self, projects: AsyncProjectsResource) -> None: + self._projects = projects + + self.create = async_to_streamed_response_wrapper( + projects.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + projects.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + projects.update, + ) + self.list = async_to_streamed_response_wrapper( + projects.list, + ) + self.delete = async_to_streamed_response_wrapper( + projects.delete, + ) + + @cached_property + def limits(self) -> AsyncLimitsResourceWithStreamingResponse: + """Create and manage projects for resource isolation within an organization.""" + return AsyncLimitsResourceWithStreamingResponse(self._projects.limits) diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py index 0e740fb..91e68d6 100644 --- a/src/kernel/types/__init__.py +++ b/src/kernel/types/__init__.py @@ -14,6 +14,7 @@ BrowserExtension as BrowserExtension, ) from .profile import Profile as Profile +from .project import Project as Project from .credential import Credential as Credential from .browser_pool import BrowserPool as BrowserPool from .browser_usage import BrowserUsage as BrowserUsage @@ -25,6 +26,7 @@ from .browser_persistence import BrowserPersistence as BrowserPersistence from .credential_provider import CredentialProvider as CredentialProvider from .profile_list_params import ProfileListParams as ProfileListParams +from .project_list_params import ProjectListParams as ProjectListParams from .proxy_create_params import ProxyCreateParams as ProxyCreateParams from .proxy_list_response import ProxyListResponse as ProxyListResponse from .proxy_check_response import ProxyCheckResponse as ProxyCheckResponse @@ -33,6 +35,8 @@ from .browser_list_response import BrowserListResponse as BrowserListResponse from .browser_update_params import BrowserUpdateParams as BrowserUpdateParams from .profile_create_params import ProfileCreateParams as ProfileCreateParams +from .project_create_params import ProjectCreateParams as ProjectCreateParams +from .project_update_params import ProjectUpdateParams as ProjectUpdateParams from .proxy_create_response import ProxyCreateResponse as ProxyCreateResponse from .credential_list_params import CredentialListParams as CredentialListParams from .deployment_list_params import DeploymentListParams as DeploymentListParams diff --git a/src/kernel/types/browser_create_response.py b/src/kernel/types/browser_create_response.py index d59a3d0..9356bb0 100644 --- a/src/kernel/types/browser_create_response.py +++ b/src/kernel/types/browser_create_response.py @@ -35,6 +35,9 @@ class BrowserCreateResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_list_response.py b/src/kernel/types/browser_list_response.py index 708caa9..f3a88f2 100644 --- a/src/kernel/types/browser_list_response.py +++ b/src/kernel/types/browser_list_response.py @@ -35,6 +35,9 @@ class BrowserListResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_pool_acquire_response.py b/src/kernel/types/browser_pool_acquire_response.py index 5ab52b5..064c405 100644 --- a/src/kernel/types/browser_pool_acquire_response.py +++ b/src/kernel/types/browser_pool_acquire_response.py @@ -35,6 +35,9 @@ class BrowserPoolAcquireResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_retrieve_response.py b/src/kernel/types/browser_retrieve_response.py index 221eab5..5b5a891 100644 --- a/src/kernel/types/browser_retrieve_response.py +++ b/src/kernel/types/browser_retrieve_response.py @@ -35,6 +35,9 @@ class BrowserRetrieveResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/browser_update_response.py b/src/kernel/types/browser_update_response.py index c8a85c3..188895a 100644 --- a/src/kernel/types/browser_update_response.py +++ b/src/kernel/types/browser_update_response.py @@ -35,6 +35,9 @@ class BrowserUpdateResponse(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/invocation_list_browsers_response.py b/src/kernel/types/invocation_list_browsers_response.py index a0fed9a..23eda77 100644 --- a/src/kernel/types/invocation_list_browsers_response.py +++ b/src/kernel/types/invocation_list_browsers_response.py @@ -35,6 +35,9 @@ class Browser(BaseModel): webdriver_ws_url: str """Websocket URL for WebDriver BiDi connections to the browser session""" + base_url: Optional[str] = None + """Metro-API HTTP base URL for this browser session.""" + browser_live_view_url: Optional[str] = None """Remote URL for live viewing the browser session. diff --git a/src/kernel/types/project.py b/src/kernel/types/project.py new file mode 100644 index 0000000..db2a377 --- /dev/null +++ b/src/kernel/types/project.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Project"] + + +class Project(BaseModel): + id: str + """Unique project identifier""" + + created_at: datetime + """When the project was created""" + + name: str + """Project name""" + + status: Literal["active", "archived"] + """Project status""" + + updated_at: datetime + """When the project was last updated""" diff --git a/src/kernel/types/project_create_params.py b/src/kernel/types/project_create_params.py new file mode 100644 index 0000000..99a5986 --- /dev/null +++ b/src/kernel/types/project_create_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ProjectCreateParams"] + + +class ProjectCreateParams(TypedDict, total=False): + name: Required[str] + """Project name (1-255 characters)""" diff --git a/src/kernel/types/project_list_params.py b/src/kernel/types/project_list_params.py new file mode 100644 index 0000000..ea10f07 --- /dev/null +++ b/src/kernel/types/project_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ProjectListParams"] + + +class ProjectListParams(TypedDict, total=False): + limit: int + """Maximum number of results to return""" + + offset: int + """Number of results to skip""" diff --git a/src/kernel/types/project_update_params.py b/src/kernel/types/project_update_params.py new file mode 100644 index 0000000..ea7de90 --- /dev/null +++ b/src/kernel/types/project_update_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ProjectUpdateParams"] + + +class ProjectUpdateParams(TypedDict, total=False): + name: str + """New project name""" + + status: Literal["active", "archived"] + """New project status""" diff --git a/src/kernel/types/projects/__init__.py b/src/kernel/types/projects/__init__.py new file mode 100644 index 0000000..acf030c --- /dev/null +++ b/src/kernel/types/projects/__init__.py @@ -0,0 +1,6 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .project_limits import ProjectLimits as ProjectLimits +from .limit_update_params import LimitUpdateParams as LimitUpdateParams diff --git a/src/kernel/types/projects/limit_update_params.py b/src/kernel/types/projects/limit_update_params.py new file mode 100644 index 0000000..6f0ec8a --- /dev/null +++ b/src/kernel/types/projects/limit_update_params.py @@ -0,0 +1,34 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["LimitUpdateParams"] + + +class LimitUpdateParams(TypedDict, total=False): + max_concurrent_invocations: Optional[int] + """Maximum concurrent app invocations for this project. + + Set to 0 to remove the cap; omit to leave unchanged. + """ + + max_concurrent_sessions: Optional[int] + """Maximum concurrent browser sessions for this project. + + Set to 0 to remove the cap; omit to leave unchanged. + """ + + max_persistent_sessions: Optional[int] + """Maximum persistent browser sessions for this project. + + Set to 0 to remove the cap; omit to leave unchanged. + """ + + max_pooled_sessions: Optional[int] + """Maximum pooled sessions capacity for this project. + + Set to 0 to remove the cap; omit to leave unchanged. + """ diff --git a/src/kernel/types/projects/project_limits.py b/src/kernel/types/projects/project_limits.py new file mode 100644 index 0000000..bf49b2a --- /dev/null +++ b/src/kernel/types/projects/project_limits.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ProjectLimits"] + + +class ProjectLimits(BaseModel): + max_concurrent_invocations: Optional[int] = None + """Maximum concurrent app invocations for this project. + + Null means no project-level cap. + """ + + max_concurrent_sessions: Optional[int] = None + """Maximum concurrent browser sessions for this project. + + Null means no project-level cap. + """ + + max_persistent_sessions: Optional[int] = None + """Maximum persistent browser sessions for this project. + + Null means no project-level cap. + """ + + max_pooled_sessions: Optional[int] = None + """Maximum pooled sessions capacity for this project. + + Null means no project-level cap. + """ diff --git a/tests/api_resources/projects/__init__.py b/tests/api_resources/projects/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/projects/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/projects/test_limits.py b/tests/api_resources/projects/test_limits.py new file mode 100644 index 0000000..9df6a0d --- /dev/null +++ b/tests/api_resources/projects/test_limits.py @@ -0,0 +1,216 @@ +# 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.projects import ProjectLimits + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLimits: + 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: + limit = client.projects.limits.retrieve( + "id", + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.projects.limits.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.projects.limits.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = response.parse() + assert_matches_type(ProjectLimits, limit, 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 `id` but received ''"): + client.projects.limits.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + limit = client.projects.limits.update( + id="id", + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + limit = client.projects.limits.update( + id="id", + max_concurrent_invocations=0, + max_concurrent_sessions=0, + max_persistent_sessions=0, + max_pooled_sessions=0, + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.projects.limits.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.projects.limits.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.limits.with_raw_response.update( + id="", + ) + + +class TestAsyncLimits: + 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: + limit = await async_client.projects.limits.retrieve( + "id", + ) + assert_matches_type(ProjectLimits, limit, 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.projects.limits.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = await response.parse() + assert_matches_type(ProjectLimits, limit, 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.projects.limits.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = await response.parse() + assert_matches_type(ProjectLimits, limit, 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 `id` but received ''"): + await async_client.projects.limits.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + limit = await async_client.projects.limits.update( + id="id", + ) + assert_matches_type(ProjectLimits, limit, 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: + limit = await async_client.projects.limits.update( + id="id", + max_concurrent_invocations=0, + max_concurrent_sessions=0, + max_persistent_sessions=0, + max_pooled_sessions=0, + ) + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.limits.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + limit = await response.parse() + assert_matches_type(ProjectLimits, limit, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.projects.limits.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + limit = await response.parse() + assert_matches_type(ProjectLimits, limit, 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_update(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.limits.with_raw_response.update( + id="", + ) diff --git a/tests/api_resources/test_projects.py b/tests/api_resources/test_projects.py new file mode 100644 index 0000000..488c191 --- /dev/null +++ b/tests/api_resources/test_projects.py @@ -0,0 +1,439 @@ +# 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 Project +from kernel.pagination import SyncOffsetPagination, AsyncOffsetPagination + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestProjects: + 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_create(self, client: Kernel) -> None: + project = client.projects.create( + name="staging", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Kernel) -> None: + response = client.projects.with_raw_response.create( + name="staging", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Kernel) -> None: + with client.projects.with_streaming_response.create( + name="staging", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Kernel) -> None: + project = client.projects.retrieve( + "id", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Kernel) -> None: + response = client.projects.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Kernel) -> None: + with client.projects.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(Project, project, 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 `id` but received ''"): + client.projects.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Kernel) -> None: + project = client.projects.update( + id="id", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Kernel) -> None: + project = client.projects.update( + id="id", + name="name", + status="active", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Kernel) -> None: + response = client.projects.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Kernel) -> None: + with client.projects.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Kernel) -> None: + project = client.projects.list() + assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Kernel) -> None: + project = client.projects.list( + limit=100, + offset=0, + ) + assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Kernel) -> None: + response = client.projects.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Kernel) -> None: + with client.projects.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert_matches_type(SyncOffsetPagination[Project], project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Kernel) -> None: + project = client.projects.delete( + "id", + ) + assert project is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Kernel) -> None: + response = client.projects.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = response.parse() + assert project is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Kernel) -> None: + with client.projects.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = response.parse() + assert project is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Kernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.projects.with_raw_response.delete( + "", + ) + + +class TestAsyncProjects: + 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_create(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.create( + name="staging", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.with_raw_response.create( + name="staging", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncKernel) -> None: + async with async_client.projects.with_streaming_response.create( + name="staging", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.retrieve( + "id", + ) + assert_matches_type(Project, project, 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.projects.with_raw_response.retrieve( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(Project, project, 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.projects.with_streaming_response.retrieve( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(Project, project, 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 `id` but received ''"): + await async_client.projects.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.update( + id="id", + ) + assert_matches_type(Project, project, 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: + project = await async_client.projects.update( + id="id", + name="name", + status="active", + ) + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(Project, project, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncKernel) -> None: + async with async_client.projects.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(Project, project, 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_update(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.list() + assert_matches_type(AsyncOffsetPagination[Project], project, 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: + project = await async_client.projects.list( + limit=100, + offset=0, + ) + assert_matches_type(AsyncOffsetPagination[Project], project, 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.projects.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert_matches_type(AsyncOffsetPagination[Project], project, 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.projects.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert_matches_type(AsyncOffsetPagination[Project], project, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncKernel) -> None: + project = await async_client.projects.delete( + "id", + ) + assert project is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncKernel) -> None: + response = await async_client.projects.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + project = await response.parse() + assert project is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncKernel) -> None: + async with async_client.projects.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + project = await response.parse() + assert project is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncKernel) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.projects.with_raw_response.delete( + "", + ) diff --git a/tests/test_client.py b/tests/test_client.py index d2eca05..c3a1186 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -427,6 +427,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: Kernel) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Kernel) -> None: request = client._build_request( FinalRequestOptions( @@ -1330,6 +1354,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncKernel) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Kernel) -> None: request = client._build_request( FinalRequestOptions(