diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4208b5c..1b77f50 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.7.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index e42a5ca..d64cbf2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 160 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gitpod%2Fgitpod-3935e467f9c15925790aada293124db82bb5d6840eeac52d81fbac6a9b0fd154.yml -openapi_spec_hash: b417d7f10ea430216e9b70e4468a3212 -config_hash: d3267594264bfb76d2ee7e881d5f8a5a +configured_endpoints: 161 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gitpod%2Fgitpod-baa13045a9492d958fc06db0dcee2fd99972435f8b9a707831cf4da8d84db194.yml +openapi_spec_hash: 5e7adb5d5cdf924eb7c0e4ddf1b81260 +config_hash: d930f7e17a525d153b810339251607b7 diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b615d..adb2ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 0.7.0 (2026-01-16) + +Full Changelog: [v0.6.0...v0.7.0](https://github.com/gitpod-io/gitpod-sdk-python/compare/v0.6.0...v0.7.0) + +### Features + +* [api] sorting for `ListMembers` ([838e74c](https://github.com/gitpod-io/gitpod-sdk-python/commit/838e74c4da4b57590a6dd0af19bdd20faf7d2805)) +* [backend] Introduce role and member status filtering for `ListMembers` ([34fb372](https://github.com/gitpod-io/gitpod-sdk-python/commit/34fb372aef655ae57fc99d5b37e152c75d831af5)) +* **api:** add draft and state fields to PullRequest proto ([e0023da](https://github.com/gitpod-io/gitpod-sdk-python/commit/e0023da5a30344c2fc87ebce55e26101c4ad40b5)) +* **api:** add ListSCMOrganizations endpoint ([9c8f7ea](https://github.com/gitpod-io/gitpod-sdk-python/commit/9c8f7eadd38bc0326ecf1be48706003fa258257c)) +* **api:** add search, creator, and status filters to ListWorkflows ([ddd18c0](https://github.com/gitpod-io/gitpod-sdk-python/commit/ddd18c09beb0f24e076818783d2dae09ca9b9f8b)) +* **api:** improve SearchRepositories pagination with next_page and total_count ([2847a10](https://github.com/gitpod-io/gitpod-sdk-python/commit/2847a10e6cbb09be83b012e8a6fcabd32f49e019)) +* **client:** add support for binary request streaming ([be5a823](https://github.com/gitpod-io/gitpod-sdk-python/commit/be5a8235224ff1ecf25464e716191fbf3c7c7fb1)) +* **dashboard:** show tier badge in org selector ([89fd8fe](https://github.com/gitpod-io/gitpod-sdk-python/commit/89fd8fef7f9de200e4aecd563c965d4209427052)) +* Define SCIMConfiguration database schema ([03bd185](https://github.com/gitpod-io/gitpod-sdk-python/commit/03bd1858ec2aefbd4c20a71c206135c441afa99c)) +* move agent mode from Spec to Status, add AgentModeChange signals ([a55115b](https://github.com/gitpod-io/gitpod-sdk-python/commit/a55115ba054078dcb689222cc150b2b1f56077bf)) +* **secrets:** add ServiceAccountSecret entity with full support ([30e17c5](https://github.com/gitpod-io/gitpod-sdk-python/commit/30e17c55b991286527f64c8857b04dd9b5a2ba7b)) + ## 0.6.0 (2026-01-09) Full Changelog: [v0.5.2...v0.6.0](https://github.com/gitpod-io/gitpod-sdk-python/compare/v0.5.2...v0.6.0) diff --git a/api.md b/api.md index 29f93a4..ed7817a 100644 --- a/api.md +++ b/api.md @@ -10,11 +10,13 @@ from gitpod.types import ( FieldValue, Gateway, OrganizationRole, + OrganizationTier, Principal, ProjectEnvironmentClass, ResourceType, RunsOn, SecretRef, + State, Subject, Task, TaskExecution, @@ -337,7 +339,6 @@ from gitpod.types import ( InviteDomains, Organization, OrganizationMember, - OrganizationTier, OrganizationCreateResponse, OrganizationRetrieveResponse, OrganizationUpdateResponse, @@ -567,6 +568,7 @@ from gitpod.types import ( RunnerCheckAuthenticationForHostResponse, RunnerCreateLogsTokenResponse, RunnerCreateRunnerTokenResponse, + RunnerListScmOrganizationsResponse, RunnerParseContextURLResponse, RunnerSearchRepositoriesResponse, ) @@ -582,6 +584,7 @@ Methods: - client.runners.check_authentication_for_host(\*\*params) -> RunnerCheckAuthenticationForHostResponse - client.runners.create_logs_token(\*\*params) -> RunnerCreateLogsTokenResponse - client.runners.create_runner_token(\*\*params) -> RunnerCreateRunnerTokenResponse +- client.runners.list_scm_organizations(\*\*params) -> RunnerListScmOrganizationsResponse - client.runners.parse_context_url(\*\*params) -> RunnerParseContextURLResponse - client.runners.search_repositories(\*\*params) -> RunnerSearchRepositoriesResponse diff --git a/pyproject.toml b/pyproject.toml index 6136eaf..4f4d826 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gitpod-sdk" -version = "0.6.0" +version = "0.7.0" description = "The official Python library for the gitpod API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/gitpod/_base_client.py b/src/gitpod/_base_client.py index 1613979..e6ccedf 100644 --- a/src/gitpod/_base_client.py +++ b/src/gitpod/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/gitpod/_models.py b/src/gitpod/_models.py index ca9500b..29070e0 100644 --- a/src/gitpod/_models.py +++ b/src/gitpod/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/gitpod/_types.py b/src/gitpod/_types.py index ab7cbe2..dbebac0 100644 --- a/src/gitpod/_types.py +++ b/src/gitpod/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/src/gitpod/_version.py b/src/gitpod/_version.py index 850eb95..88739dd 100644 --- a/src/gitpod/_version.py +++ b/src/gitpod/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "gitpod" -__version__ = "0.6.0" # x-release-please-version +__version__ = "0.7.0" # x-release-please-version diff --git a/src/gitpod/resources/organizations/organizations.py b/src/gitpod/resources/organizations/organizations.py index c1ec57e..c9de4ae 100644 --- a/src/gitpod/resources/organizations/organizations.py +++ b/src/gitpod/resources/organizations/organizations.py @@ -507,6 +507,7 @@ def list_members( page_size: int | Omit = omit, filter: organization_list_members_params.Filter | Omit = omit, pagination: organization_list_members_params.Pagination | Omit = omit, + sort: organization_list_members_params.Sort | 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, @@ -551,6 +552,11 @@ def list_members( pagination: pagination contains the pagination options for listing members + sort: sort specifies the order of results. When unspecified, the authenticated user is + returned first, followed by other members sorted by name ascending. When an + explicit sort is specified, results are sorted purely by the requested field + without any special handling for the authenticated user. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -567,6 +573,7 @@ def list_members( "organization_id": organization_id, "filter": filter, "pagination": pagination, + "sort": sort, }, organization_list_members_params.OrganizationListMembersParams, ), @@ -1086,6 +1093,7 @@ def list_members( page_size: int | Omit = omit, filter: organization_list_members_params.Filter | Omit = omit, pagination: organization_list_members_params.Pagination | Omit = omit, + sort: organization_list_members_params.Sort | 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, @@ -1130,6 +1138,11 @@ def list_members( pagination: pagination contains the pagination options for listing members + sort: sort specifies the order of results. When unspecified, the authenticated user is + returned first, followed by other members sorted by name ascending. When an + explicit sort is specified, results are sorted purely by the requested field + without any special handling for the authenticated user. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -1146,6 +1159,7 @@ def list_members( "organization_id": organization_id, "filter": filter, "pagination": pagination, + "sort": sort, }, organization_list_members_params.OrganizationListMembersParams, ), diff --git a/src/gitpod/resources/runners/runners.py b/src/gitpod/resources/runners/runners.py index 1c445f8..0ba69c1 100644 --- a/src/gitpod/resources/runners/runners.py +++ b/src/gitpod/resources/runners/runners.py @@ -19,6 +19,7 @@ runner_parse_context_url_params, runner_create_runner_token_params, runner_search_repositories_params, + runner_list_scm_organizations_params, runner_check_authentication_for_host_params, ) from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given @@ -60,6 +61,7 @@ from ...types.runner_parse_context_url_response import RunnerParseContextURLResponse from ...types.runner_create_runner_token_response import RunnerCreateRunnerTokenResponse from ...types.runner_search_repositories_response import RunnerSearchRepositoriesResponse +from ...types.runner_list_scm_organizations_response import RunnerListScmOrganizationsResponse from ...types.runner_check_authentication_for_host_response import RunnerCheckAuthenticationForHostResponse __all__ = ["RunnersResource", "AsyncRunnersResource"] @@ -608,6 +610,75 @@ def create_runner_token( cast_to=RunnerCreateRunnerTokenResponse, ) + def list_scm_organizations( + self, + *, + token: str | Omit = omit, + page_size: int | Omit = omit, + runner_id: str | Omit = omit, + scm_host: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RunnerListScmOrganizationsResponse: + """ + Lists SCM organizations the user belongs to. + + Use this method to: + + - Get all organizations for a user on a specific SCM host + - Check organization admin permissions for webhook creation + + ### Examples + + - List GitHub organizations: + + Lists all organizations the user belongs to on GitHub. + + ```yaml + runnerId: "d2c94c27-3b76-4a42-b88c-95a85e392c68" + scmHost: "github.com" + ``` + + Args: + scm_host: The SCM host to list organizations from (e.g., "github.com", "gitlab.com") + + 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( + "/gitpod.v1.RunnerService/ListSCMOrganizations", + body=maybe_transform( + { + "runner_id": runner_id, + "scm_host": scm_host, + }, + runner_list_scm_organizations_params.RunnerListScmOrganizationsParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "token": token, + "page_size": page_size, + }, + runner_list_scm_organizations_params.RunnerListScmOrganizationsParams, + ), + ), + cast_to=RunnerListScmOrganizationsResponse, + ) + def parse_context_url( self, *, @@ -1304,6 +1375,75 @@ async def create_runner_token( cast_to=RunnerCreateRunnerTokenResponse, ) + async def list_scm_organizations( + self, + *, + token: str | Omit = omit, + page_size: int | Omit = omit, + runner_id: str | Omit = omit, + scm_host: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> RunnerListScmOrganizationsResponse: + """ + Lists SCM organizations the user belongs to. + + Use this method to: + + - Get all organizations for a user on a specific SCM host + - Check organization admin permissions for webhook creation + + ### Examples + + - List GitHub organizations: + + Lists all organizations the user belongs to on GitHub. + + ```yaml + runnerId: "d2c94c27-3b76-4a42-b88c-95a85e392c68" + scmHost: "github.com" + ``` + + Args: + scm_host: The SCM host to list organizations from (e.g., "github.com", "gitlab.com") + + 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( + "/gitpod.v1.RunnerService/ListSCMOrganizations", + body=await async_maybe_transform( + { + "runner_id": runner_id, + "scm_host": scm_host, + }, + runner_list_scm_organizations_params.RunnerListScmOrganizationsParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "token": token, + "page_size": page_size, + }, + runner_list_scm_organizations_params.RunnerListScmOrganizationsParams, + ), + ), + cast_to=RunnerListScmOrganizationsResponse, + ) + async def parse_context_url( self, *, @@ -1483,6 +1623,9 @@ def __init__(self, runners: RunnersResource) -> None: self.create_runner_token = to_raw_response_wrapper( runners.create_runner_token, ) + self.list_scm_organizations = to_raw_response_wrapper( + runners.list_scm_organizations, + ) self.parse_context_url = to_raw_response_wrapper( runners.parse_context_url, ) @@ -1527,6 +1670,9 @@ def __init__(self, runners: AsyncRunnersResource) -> None: self.create_runner_token = async_to_raw_response_wrapper( runners.create_runner_token, ) + self.list_scm_organizations = async_to_raw_response_wrapper( + runners.list_scm_organizations, + ) self.parse_context_url = async_to_raw_response_wrapper( runners.parse_context_url, ) @@ -1571,6 +1717,9 @@ def __init__(self, runners: RunnersResource) -> None: self.create_runner_token = to_streamed_response_wrapper( runners.create_runner_token, ) + self.list_scm_organizations = to_streamed_response_wrapper( + runners.list_scm_organizations, + ) self.parse_context_url = to_streamed_response_wrapper( runners.parse_context_url, ) @@ -1615,6 +1764,9 @@ def __init__(self, runners: AsyncRunnersResource) -> None: self.create_runner_token = async_to_streamed_response_wrapper( runners.create_runner_token, ) + self.list_scm_organizations = async_to_streamed_response_wrapper( + runners.list_scm_organizations, + ) self.parse_context_url = async_to_streamed_response_wrapper( runners.parse_context_url, ) diff --git a/src/gitpod/types/__init__.py b/src/gitpod/types/__init__.py index bd85a24..7306513 100644 --- a/src/gitpod/types/__init__.py +++ b/src/gitpod/types/__init__.py @@ -10,6 +10,7 @@ from .secret import Secret as Secret from .shared import ( Task as Task, + State as State, RunsOn as RunsOn, Gateway as Gateway, Subject as Subject, @@ -24,6 +25,7 @@ TaskExecution as TaskExecution, EnvironmentClass as EnvironmentClass, OrganizationRole as OrganizationRole, + OrganizationTier as OrganizationTier, AutomationTrigger as AutomationTrigger, TaskExecutionSpec as TaskExecutionSpec, TaskExecutionPhase as TaskExecutionPhase, @@ -71,7 +73,6 @@ from .error_event_param import ErrorEventParam as ErrorEventParam from .event_list_params import EventListParams as EventListParams from .group_list_params import GroupListParams as GroupListParams -from .organization_tier import OrganizationTier as OrganizationTier from .prebuild_metadata import PrebuildMetadata as PrebuildMetadata from .runner_capability import RunnerCapability as RunnerCapability from .runner_spec_param import RunnerSpecParam as RunnerSpecParam @@ -220,6 +221,7 @@ from .runner_search_repositories_response import RunnerSearchRepositoriesResponse as RunnerSearchRepositoriesResponse from .environment_create_logs_token_params import EnvironmentCreateLogsTokenParams as EnvironmentCreateLogsTokenParams from .project_prebuild_configuration_param import ProjectPrebuildConfigurationParam as ProjectPrebuildConfigurationParam +from .runner_list_scm_organizations_params import RunnerListScmOrganizationsParams as RunnerListScmOrganizationsParams from .user_get_authenticated_user_response import UserGetAuthenticatedUserResponse as UserGetAuthenticatedUserResponse from .environment_create_from_project_params import ( EnvironmentCreateFromProjectParams as EnvironmentCreateFromProjectParams, @@ -230,6 +232,9 @@ from .project_create_from_environment_params import ( ProjectCreateFromEnvironmentParams as ProjectCreateFromEnvironmentParams, ) +from .runner_list_scm_organizations_response import ( + RunnerListScmOrganizationsResponse as RunnerListScmOrganizationsResponse, +) from .environment_create_from_project_response import ( EnvironmentCreateFromProjectResponse as EnvironmentCreateFromProjectResponse, ) diff --git a/src/gitpod/types/account_membership.py b/src/gitpod/types/account_membership.py index 10f2603..6f443a8 100644 --- a/src/gitpod/types/account_membership.py +++ b/src/gitpod/types/account_membership.py @@ -6,6 +6,7 @@ from .._models import BaseModel from .shared.organization_role import OrganizationRole +from .shared.organization_tier import OrganizationTier __all__ = ["AccountMembership"] @@ -25,6 +26,9 @@ class AccountMembership(BaseModel): organization_member_count: Optional[int] = FieldInfo(alias="organizationMemberCount", default=None) """ - organization_name is the member count of the organization the user is a member - of + organization_member_count is the member count of the organization the user is a + member of """ + + organization_tier: Optional[OrganizationTier] = FieldInfo(alias="organizationTier", default=None) + """organization_tier is the tier of the organization (Free, Core, Enterprise)""" diff --git a/src/gitpod/types/agent_code_context.py b/src/gitpod/types/agent_code_context.py index 05c1832..ece7309 100644 --- a/src/gitpod/types/agent_code_context.py +++ b/src/gitpod/types/agent_code_context.py @@ -5,6 +5,7 @@ from pydantic import Field as FieldInfo from .._models import BaseModel +from .shared.state import State __all__ = ["AgentCodeContext", "ContextURL", "PullRequest", "PullRequestRepository"] @@ -40,12 +41,18 @@ class PullRequest(BaseModel): author: Optional[str] = None """Author name as provided by the SCM system""" + draft: Optional[bool] = None + """Whether this is a draft pull request""" + from_branch: Optional[str] = FieldInfo(alias="fromBranch", default=None) """Source branch name (the branch being merged from)""" repository: Optional[PullRequestRepository] = None """Repository information""" + state: Optional[State] = None + """Current state of the pull request""" + title: Optional[str] = None """Pull request title""" diff --git a/src/gitpod/types/agent_code_context_param.py b/src/gitpod/types/agent_code_context_param.py index 8080145..821f296 100644 --- a/src/gitpod/types/agent_code_context_param.py +++ b/src/gitpod/types/agent_code_context_param.py @@ -6,6 +6,7 @@ from typing_extensions import Annotated, TypedDict from .._utils import PropertyInfo +from .shared.state import State __all__ = ["AgentCodeContextParam", "ContextURL", "PullRequest", "PullRequestRepository"] @@ -41,12 +42,18 @@ class PullRequest(TypedDict, total=False): author: str """Author name as provided by the SCM system""" + draft: bool + """Whether this is a draft pull request""" + from_branch: Annotated[str, PropertyInfo(alias="fromBranch")] """Source branch name (the branch being merged from)""" repository: PullRequestRepository """Repository information""" + state: State + """Current state of the pull request""" + title: str """Pull request title""" diff --git a/src/gitpod/types/agent_execution.py b/src/gitpod/types/agent_execution.py index 384da90..ad11938 100644 --- a/src/gitpod/types/agent_execution.py +++ b/src/gitpod/types/agent_execution.py @@ -259,9 +259,6 @@ class Spec(BaseModel): limits: Optional[SpecLimits] = None - mode: Optional[AgentMode] = None - """mode is the operational mode for this agent execution""" - session: Optional[str] = None spec_version: Optional[str] = FieldInfo(alias="specVersion", default=None) @@ -355,6 +352,12 @@ class Status(BaseModel): judgement: Optional[str] = None """judgement is the judgement of the agent run produced by the judgement prompt.""" + mode: Optional[AgentMode] = None + """ + mode is the current operational mode of the agent execution. This is set by the + agent when entering different modes (e.g., Ralph mode via /ona:ralph command). + """ + outputs: Optional[Dict[str, StatusOutputs]] = None """ outputs is a map of key-value pairs that can be set by the agent during diff --git a/src/gitpod/types/agent_mode.py b/src/gitpod/types/agent_mode.py index de414c7..d7bd23b 100644 --- a/src/gitpod/types/agent_mode.py +++ b/src/gitpod/types/agent_mode.py @@ -4,4 +4,6 @@ __all__ = ["AgentMode"] -AgentMode: TypeAlias = Literal["AGENT_MODE_UNSPECIFIED", "AGENT_MODE_EXECUTION", "AGENT_MODE_PLANNING"] +AgentMode: TypeAlias = Literal[ + "AGENT_MODE_UNSPECIFIED", "AGENT_MODE_EXECUTION", "AGENT_MODE_PLANNING", "AGENT_MODE_RALPH" +] diff --git a/src/gitpod/types/groups/resource_role.py b/src/gitpod/types/groups/resource_role.py index cd2e164..33ecbb7 100644 --- a/src/gitpod/types/groups/resource_role.py +++ b/src/gitpod/types/groups/resource_role.py @@ -8,6 +8,7 @@ "RESOURCE_ROLE_UNSPECIFIED", "RESOURCE_ROLE_ORG_ADMIN", "RESOURCE_ROLE_ORG_MEMBER", + "RESOURCE_ROLE_ORG_RUNNERS_ADMIN", "RESOURCE_ROLE_GROUP_ADMIN", "RESOURCE_ROLE_GROUP_VIEWER", "RESOURCE_ROLE_USER_IDENTITY", diff --git a/src/gitpod/types/organization.py b/src/gitpod/types/organization.py index edf88f1..e833b09 100644 --- a/src/gitpod/types/organization.py +++ b/src/gitpod/types/organization.py @@ -7,7 +7,7 @@ from .._models import BaseModel from .invite_domains import InviteDomains -from .organization_tier import OrganizationTier +from .shared.organization_tier import OrganizationTier __all__ = ["Organization"] diff --git a/src/gitpod/types/organization_list_members_params.py b/src/gitpod/types/organization_list_members_params.py index 06c529e..2e5f87e 100644 --- a/src/gitpod/types/organization_list_members_params.py +++ b/src/gitpod/types/organization_list_members_params.py @@ -2,11 +2,14 @@ from __future__ import annotations -from typing_extensions import Required, Annotated, TypedDict +from typing import List +from typing_extensions import Literal, Required, Annotated, TypedDict from .._utils import PropertyInfo +from .shared.user_status import UserStatus +from .shared.organization_role import OrganizationRole -__all__ = ["OrganizationListMembersParams", "Filter", "Pagination"] +__all__ = ["OrganizationListMembersParams", "Filter", "Pagination", "Sort"] class OrganizationListMembersParams(TypedDict, total=False): @@ -22,11 +25,26 @@ class OrganizationListMembersParams(TypedDict, total=False): pagination: Pagination """pagination contains the pagination options for listing members""" + sort: Sort + """sort specifies the order of results. + + When unspecified, the authenticated user is returned first, followed by other + members sorted by name ascending. When an explicit sort is specified, results + are sorted purely by the requested field without any special handling for the + authenticated user. + """ + class Filter(TypedDict, total=False): + roles: List[OrganizationRole] + """roles filters members by their organization role""" + search: str """search performs case-insensitive search across member name and email""" + statuses: List[UserStatus] + """status filters members by their user status""" + class Pagination(TypedDict, total=False): """pagination contains the pagination options for listing members""" @@ -42,3 +60,17 @@ class Pagination(TypedDict, total=False): Maximum 100. """ + + +class Sort(TypedDict, total=False): + """sort specifies the order of results. + + When unspecified, the authenticated user is + returned first, followed by other members sorted by name ascending. When an explicit + sort is specified, results are sorted purely by the requested field without any + special handling for the authenticated user. + """ + + field: Literal["SORT_FIELD_UNSPECIFIED", "SORT_FIELD_NAME", "SORT_FIELD_DATE_JOINED"] + + order: Literal["SORT_ORDER_UNSPECIFIED", "SORT_ORDER_ASC", "SORT_ORDER_DESC"] diff --git a/src/gitpod/types/runner_list_scm_organizations_params.py b/src/gitpod/types/runner_list_scm_organizations_params.py new file mode 100644 index 0000000..bd1f788 --- /dev/null +++ b/src/gitpod/types/runner_list_scm_organizations_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["RunnerListScmOrganizationsParams"] + + +class RunnerListScmOrganizationsParams(TypedDict, total=False): + token: str + + page_size: Annotated[int, PropertyInfo(alias="pageSize")] + + runner_id: Annotated[str, PropertyInfo(alias="runnerId")] + + scm_host: Annotated[str, PropertyInfo(alias="scmHost")] + """The SCM host to list organizations from (e.g., "github.com", "gitlab.com")""" diff --git a/src/gitpod/types/runner_list_scm_organizations_response.py b/src/gitpod/types/runner_list_scm_organizations_response.py new file mode 100644 index 0000000..8e9db07 --- /dev/null +++ b/src/gitpod/types/runner_list_scm_organizations_response.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["RunnerListScmOrganizationsResponse", "Organization"] + + +class Organization(BaseModel): + is_admin: Optional[bool] = FieldInfo(alias="isAdmin", default=None) + """ + Whether the user has admin permissions in this organization. Admin permissions + typically allow creating organization-level webhooks. + """ + + name: Optional[str] = None + """Organization name/slug (e.g., "gitpod-io")""" + + url: Optional[str] = None + """Organization URL (e.g., "https://github.com/gitpod-io")""" + + +class RunnerListScmOrganizationsResponse(BaseModel): + organizations: Optional[List[Organization]] = None + """List of organizations the user belongs to""" diff --git a/src/gitpod/types/runner_parse_context_url_response.py b/src/gitpod/types/runner_parse_context_url_response.py index c712e40..c18fd7d 100644 --- a/src/gitpod/types/runner_parse_context_url_response.py +++ b/src/gitpod/types/runner_parse_context_url_response.py @@ -5,6 +5,7 @@ from pydantic import Field as FieldInfo from .._models import BaseModel +from .shared.state import State __all__ = ["RunnerParseContextURLResponse", "Git", "Issue", "Pr", "PullRequest", "PullRequestRepository"] @@ -71,12 +72,18 @@ class PullRequest(BaseModel): author: Optional[str] = None """Author name as provided by the SCM system""" + draft: Optional[bool] = None + """Whether this is a draft pull request""" + from_branch: Optional[str] = FieldInfo(alias="fromBranch", default=None) """Source branch name (the branch being merged from)""" repository: Optional[PullRequestRepository] = None """Repository information""" + state: Optional[State] = None + """Current state of the pull request""" + title: Optional[str] = None """Pull request title""" diff --git a/src/gitpod/types/runner_search_repositories_response.py b/src/gitpod/types/runner_search_repositories_response.py index 65597de..e4f7350 100644 --- a/src/gitpod/types/runner_search_repositories_response.py +++ b/src/gitpod/types/runner_search_repositories_response.py @@ -10,7 +10,12 @@ class Pagination(BaseModel): - """Pagination information for the response""" + """ + Pagination information for the response. + Token format: "NEXT_PAGE/TOTAL_PAGES/TOTAL_COUNT" (e.g., "2/40/1000"). + Use -1 for unknown values (e.g., "2/-1/-1" when totals unavailable). + Empty token means no more pages. + """ next_token: Optional[str] = FieldInfo(alias="nextToken", default=None) """Token passed for retrieving the next set of results. @@ -29,10 +34,18 @@ class Repository(BaseModel): class RunnerSearchRepositoriesResponse(BaseModel): last_page: Optional[int] = FieldInfo(alias="lastPage", default=None) - """Last page in the responses""" + """Deprecated: Use pagination token instead. + + Total pages can be extracted from token. + """ pagination: Optional[Pagination] = None - """Pagination information for the response""" + """ + Pagination information for the response. Token format: + "NEXT_PAGE/TOTAL_PAGES/TOTAL_COUNT" (e.g., "2/40/1000"). Use -1 for unknown + values (e.g., "2/-1/-1" when totals unavailable). Empty token means no more + pages. + """ repositories: Optional[List[Repository]] = None """List of repositories matching the search criteria""" diff --git a/src/gitpod/types/secret_scope.py b/src/gitpod/types/secret_scope.py index f66b672..1d01cb3 100644 --- a/src/gitpod/types/secret_scope.py +++ b/src/gitpod/types/secret_scope.py @@ -16,5 +16,8 @@ class SecretScope(BaseModel): project_id: Optional[str] = FieldInfo(alias="projectId", default=None) """project_id is the Project ID this Secret belongs to""" + service_account_id: Optional[str] = FieldInfo(alias="serviceAccountId", default=None) + """service_account_id is the Service Account ID this Secret belongs to""" + user_id: Optional[str] = FieldInfo(alias="userId", default=None) """user_id is the User ID this Secret belongs to""" diff --git a/src/gitpod/types/secret_scope_param.py b/src/gitpod/types/secret_scope_param.py index d68e56b..b7cf4f4 100644 --- a/src/gitpod/types/secret_scope_param.py +++ b/src/gitpod/types/secret_scope_param.py @@ -16,5 +16,8 @@ class SecretScopeParam(TypedDict, total=False): project_id: Annotated[str, PropertyInfo(alias="projectId")] """project_id is the Project ID this Secret belongs to""" + service_account_id: Annotated[str, PropertyInfo(alias="serviceAccountId")] + """service_account_id is the Service Account ID this Secret belongs to""" + user_id: Annotated[str, PropertyInfo(alias="userId")] """user_id is the User ID this Secret belongs to""" diff --git a/src/gitpod/types/shared/__init__.py b/src/gitpod/types/shared/__init__.py index 50c1315..de9766f 100644 --- a/src/gitpod/types/shared/__init__.py +++ b/src/gitpod/types/shared/__init__.py @@ -1,6 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from .task import Task as Task +from .state import State as State from .gateway import Gateway as Gateway from .runs_on import RunsOn as RunsOn from .subject import Subject as Subject @@ -15,6 +16,7 @@ from .task_execution import TaskExecution as TaskExecution from .environment_class import EnvironmentClass as EnvironmentClass from .organization_role import OrganizationRole as OrganizationRole +from .organization_tier import OrganizationTier as OrganizationTier from .automation_trigger import AutomationTrigger as AutomationTrigger from .task_execution_spec import TaskExecutionSpec as TaskExecutionSpec from .task_execution_phase import TaskExecutionPhase as TaskExecutionPhase diff --git a/src/gitpod/types/organization_tier.py b/src/gitpod/types/shared/organization_tier.py similarity index 100% rename from src/gitpod/types/organization_tier.py rename to src/gitpod/types/shared/organization_tier.py diff --git a/src/gitpod/types/shared/resource_type.py b/src/gitpod/types/shared/resource_type.py index 1a18f4e..599108b 100644 --- a/src/gitpod/types/shared/resource_type.py +++ b/src/gitpod/types/shared/resource_type.py @@ -48,4 +48,6 @@ "RESOURCE_TYPE_ROLE_ASSIGNMENT_CHANGED", "RESOURCE_TYPE_GROUP_MEMBERSHIP_CHANGED", "RESOURCE_TYPE_WEBHOOK", + "RESOURCE_TYPE_SCIM_CONFIGURATION", + "RESOURCE_TYPE_SERVICE_ACCOUNT_SECRET", ] diff --git a/src/gitpod/types/shared/state.py b/src/gitpod/types/shared/state.py new file mode 100644 index 0000000..92aa0de --- /dev/null +++ b/src/gitpod/types/shared/state.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal, TypeAlias + +__all__ = ["State"] + +State: TypeAlias = Literal["STATE_UNSPECIFIED", "STATE_OPEN", "STATE_CLOSED", "STATE_MERGED"] diff --git a/src/gitpod/types/shared_params/__init__.py b/src/gitpod/types/shared_params/__init__.py index 4673c76..093bf76 100644 --- a/src/gitpod/types/shared_params/__init__.py +++ b/src/gitpod/types/shared_params/__init__.py @@ -1,11 +1,13 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from .state import State as State from .runs_on import RunsOn as RunsOn from .subject import Subject as Subject from .principal import Principal as Principal from .task_spec import TaskSpec as TaskSpec from .secret_ref import SecretRef as SecretRef from .field_value import FieldValue as FieldValue +from .user_status import UserStatus as UserStatus from .resource_type import ResourceType as ResourceType from .task_metadata import TaskMetadata as TaskMetadata from .environment_class import EnvironmentClass as EnvironmentClass diff --git a/src/gitpod/types/shared_params/resource_type.py b/src/gitpod/types/shared_params/resource_type.py index 793bab2..9d489e4 100644 --- a/src/gitpod/types/shared_params/resource_type.py +++ b/src/gitpod/types/shared_params/resource_type.py @@ -50,4 +50,6 @@ "RESOURCE_TYPE_ROLE_ASSIGNMENT_CHANGED", "RESOURCE_TYPE_GROUP_MEMBERSHIP_CHANGED", "RESOURCE_TYPE_WEBHOOK", + "RESOURCE_TYPE_SCIM_CONFIGURATION", + "RESOURCE_TYPE_SERVICE_ACCOUNT_SECRET", ] diff --git a/src/gitpod/types/shared_params/state.py b/src/gitpod/types/shared_params/state.py new file mode 100644 index 0000000..1d81f94 --- /dev/null +++ b/src/gitpod/types/shared_params/state.py @@ -0,0 +1,9 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypeAlias + +__all__ = ["State"] + +State: TypeAlias = Literal["STATE_UNSPECIFIED", "STATE_OPEN", "STATE_CLOSED", "STATE_MERGED"] diff --git a/src/gitpod/types/shared_params/user_status.py b/src/gitpod/types/shared_params/user_status.py new file mode 100644 index 0000000..fd8ac19 --- /dev/null +++ b/src/gitpod/types/shared_params/user_status.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypeAlias + +__all__ = ["UserStatus"] + +UserStatus: TypeAlias = Literal[ + "USER_STATUS_UNSPECIFIED", "USER_STATUS_ACTIVE", "USER_STATUS_SUSPENDED", "USER_STATUS_LEFT" +] diff --git a/tests/api_resources/test_agents.py b/tests/api_resources/test_agents.py index b92173b..f4f2aad 100644 --- a/tests/api_resources/test_agents.py +++ b/tests/api_resources/test_agents.py @@ -410,6 +410,7 @@ def test_method_start_execution_with_all_params(self, client: Gitpod) -> None: "pull_request": { "id": "id", "author": "author", + "draft": True, "from_branch": "fromBranch", "repository": { "clone_url": "cloneUrl", @@ -417,6 +418,7 @@ def test_method_start_execution_with_all_params(self, client: Gitpod) -> None: "name": "name", "owner": "owner", }, + "state": "STATE_UNSPECIFIED", "title": "title", "to_branch": "toBranch", "url": "url", @@ -921,6 +923,7 @@ async def test_method_start_execution_with_all_params(self, async_client: AsyncG "pull_request": { "id": "id", "author": "author", + "draft": True, "from_branch": "fromBranch", "repository": { "clone_url": "cloneUrl", @@ -928,6 +931,7 @@ async def test_method_start_execution_with_all_params(self, async_client: AsyncG "name": "name", "owner": "owner", }, + "state": "STATE_UNSPECIFIED", "title": "title", "to_branch": "toBranch", "url": "url", diff --git a/tests/api_resources/test_organizations.py b/tests/api_resources/test_organizations.py index 8cda4e7..b404eb7 100644 --- a/tests/api_resources/test_organizations.py +++ b/tests/api_resources/test_organizations.py @@ -266,11 +266,19 @@ def test_method_list_members_with_all_params(self, client: Gitpod) -> None: organization_id="b0e12f6c-4c67-429d-a4a6-d9838b5da047", token="token", page_size=0, - filter={"search": "search"}, + filter={ + "roles": ["ORGANIZATION_ROLE_UNSPECIFIED"], + "search": "search", + "statuses": ["USER_STATUS_UNSPECIFIED"], + }, pagination={ "token": "token", "page_size": 20, }, + sort={ + "field": "SORT_FIELD_UNSPECIFIED", + "order": "SORT_ORDER_UNSPECIFIED", + }, ) assert_matches_type(SyncMembersPage[OrganizationMember], organization, path=["response"]) @@ -595,11 +603,19 @@ async def test_method_list_members_with_all_params(self, async_client: AsyncGitp organization_id="b0e12f6c-4c67-429d-a4a6-d9838b5da047", token="token", page_size=0, - filter={"search": "search"}, + filter={ + "roles": ["ORGANIZATION_ROLE_UNSPECIFIED"], + "search": "search", + "statuses": ["USER_STATUS_UNSPECIFIED"], + }, pagination={ "token": "token", "page_size": 20, }, + sort={ + "field": "SORT_FIELD_UNSPECIFIED", + "order": "SORT_ORDER_UNSPECIFIED", + }, ) assert_matches_type(AsyncMembersPage[OrganizationMember], organization, path=["response"]) diff --git a/tests/api_resources/test_runners.py b/tests/api_resources/test_runners.py index 9ce4674..5cf72fe 100644 --- a/tests/api_resources/test_runners.py +++ b/tests/api_resources/test_runners.py @@ -17,6 +17,7 @@ RunnerParseContextURLResponse, RunnerCreateRunnerTokenResponse, RunnerSearchRepositoriesResponse, + RunnerListScmOrganizationsResponse, RunnerCheckAuthenticationForHostResponse, ) from gitpod.pagination import SyncRunnersPage, AsyncRunnersPage @@ -363,6 +364,45 @@ def test_streaming_response_create_runner_token(self, client: Gitpod) -> None: assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_scm_organizations(self, client: Gitpod) -> None: + runner = client.runners.list_scm_organizations() + assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list_scm_organizations_with_all_params(self, client: Gitpod) -> None: + runner = client.runners.list_scm_organizations( + token="token", + page_size=0, + runner_id="d2c94c27-3b76-4a42-b88c-95a85e392c68", + scm_host="github.com", + ) + assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list_scm_organizations(self, client: Gitpod) -> None: + response = client.runners.with_raw_response.list_scm_organizations() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + runner = response.parse() + assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list_scm_organizations(self, client: Gitpod) -> None: + with client.runners.with_streaming_response.list_scm_organizations() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + runner = response.parse() + assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize def test_method_parse_context_url(self, client: Gitpod) -> None: @@ -786,6 +826,45 @@ async def test_streaming_response_create_runner_token(self, async_client: AsyncG assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_scm_organizations(self, async_client: AsyncGitpod) -> None: + runner = await async_client.runners.list_scm_organizations() + assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list_scm_organizations_with_all_params(self, async_client: AsyncGitpod) -> None: + runner = await async_client.runners.list_scm_organizations( + token="token", + page_size=0, + runner_id="d2c94c27-3b76-4a42-b88c-95a85e392c68", + scm_host="github.com", + ) + assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list_scm_organizations(self, async_client: AsyncGitpod) -> None: + response = await async_client.runners.with_raw_response.list_scm_organizations() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + runner = await response.parse() + assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list_scm_organizations(self, async_client: AsyncGitpod) -> None: + async with async_client.runners.with_streaming_response.list_scm_organizations() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + runner = await response.parse() + assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism tests are disabled") @parametrize async def test_method_parse_context_url(self, async_client: AsyncGitpod) -> None: diff --git a/tests/api_resources/test_secrets.py b/tests/api_resources/test_secrets.py index 6a54d36..680a9d7 100644 --- a/tests/api_resources/test_secrets.py +++ b/tests/api_resources/test_secrets.py @@ -41,6 +41,7 @@ def test_method_create_with_all_params(self, client: Gitpod) -> None: scope={ "organization_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "project_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "service_account_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "user_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", }, value="postgresql://user:pass@localhost:5432/db", @@ -86,6 +87,7 @@ def test_method_list_with_all_params(self, client: Gitpod) -> None: "scope": { "organization_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "project_id": "b0e12f6c-4c67-429d-a4a6-d9838b5da047", + "service_account_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "user_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", }, }, @@ -252,6 +254,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncGitpod) -> scope={ "organization_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "project_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + "service_account_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "user_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", }, value="postgresql://user:pass@localhost:5432/db", @@ -297,6 +300,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncGitpod) -> N "scope": { "organization_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "project_id": "b0e12f6c-4c67-429d-a4a6-d9838b5da047", + "service_account_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", "user_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", }, }, diff --git a/tests/test_client.py b/tests/test_client.py index f948b8a..81db6c8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") bearer_token = "My Bearer Token" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: Gitpod | AsyncGitpod) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -511,6 +564,70 @@ def test_multipart_repeating_array(self, client: Gitpod) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Gitpod) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with Gitpod( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Gitpod) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: Gitpod) -> None: class Model1(BaseModel): @@ -1364,6 +1481,72 @@ def test_multipart_repeating_array(self, async_client: AsyncGitpod) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncGitpod) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncGitpod( + base_url=base_url, + bearer_token=bearer_token, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncGitpod + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncGitpod) -> None: class Model1(BaseModel):