diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index f7014c35..a7130553 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.11.0"
+ ".": "0.12.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index e93723a0..2a5eaf4a 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 193
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gitpod%2Fgitpod-3dcdbd68ce4b336149d28d17ab08f211538ed6630112ae4883af2f6680643159.yml
-openapi_spec_hash: 7e4333995b65cf32663166801e2444bb
-config_hash: 8d7b241284195a8c51f5d670fbbe0ab4
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gitpod%2Fgitpod-c5b3669c8db150b04d14f02596f3fd59076b7ac971fbb66583231a1189a8c91b.yml
+openapi_spec_hash: a4114b38d47f0696fdf23bfe64dc446c
+config_hash: 4447d1e1149a80d1bec70d353fb8acbf
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ddc444e4..1e07c1e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,44 @@
# Changelog
+## 0.12.0 (2026-04-24)
+
+Full Changelog: [v0.11.0...v0.12.0](https://github.com/gitpod-io/gitpod-sdk-python/compare/v0.11.0...v0.12.0)
+
+### Features
+
+* **api:** add integration_id field, make webhook_id required in pull_request trigger ([077b662](https://github.com/gitpod-io/gitpod-sdk-python/commit/077b6622dc4e21a2033c8275c46716286a6515b8))
+* **api:** add old_path field to ContentGitChangedFile ([d79d4d4](https://github.com/gitpod-io/gitpod-sdk-python/commit/d79d4d49ef41a889a81a0d484616fca442849356))
+* **api:** add pagination and query parameter to runners.list_scm_organizations ([333311a](https://github.com/gitpod-io/gitpod-sdk-python/commit/333311aebaefb657861e2a9f6e8adb0f2abdd4dc))
+* **api:** add port_authentication capability to runner_capability ([c29b095](https://github.com/gitpod-io/gitpod-sdk-python/commit/c29b09596d2d87caaea047e61613a333c2fe4e31))
+* **api:** add prebuild trigger value to environments automations ([af2c44e](https://github.com/gitpod-io/gitpod-sdk-python/commit/af2c44e64fac19bf848c4325e0b39b183c998e74))
+* **api:** add PULL_REQUEST_EVENT_REVIEW_REQUESTED to workflow_trigger events ([242a3ab](https://github.com/gitpod-io/gitpod-sdk-python/commit/242a3ab60ed3580fd9488858727294ed86568ccf))
+* **api:** add SUPPORTED_MODEL_OPENAI_AUTO to agent_execution status ([54503f5](https://github.com/gitpod-io/gitpod-sdk-python/commit/54503f5295a703ee855eac4c11694d2bbe465d13))
+* **api:** add SUPPORTED_MODEL_OPUS_4_7 to agent_execution Status ([74af533](https://github.com/gitpod-io/gitpod-sdk-python/commit/74af5338d7f6447df5a6464a2b2ef893c3bf4f6e))
+* **api:** add UserInputMetadata type ([ea300f4](https://github.com/gitpod-io/gitpod-sdk-python/commit/ea300f4314c14529c02ffec1e38f474d8a426844))
+* **api:** remove terminal field from RunsOn type ([faca2b2](https://github.com/gitpod-io/gitpod-sdk-python/commit/faca2b27b88f17a759bd91c305e5ea2a856a1e2c))
+
+
+### Bug Fixes
+
+* **client:** preserve hardcoded query params when merging with user params ([b7f0b1d](https://github.com/gitpod-io/gitpod-sdk-python/commit/b7f0b1d27ef51872bf81541dd7f81a8101f856af))
+* ensure file data are only sent as 1 parameter ([5c02854](https://github.com/gitpod-io/gitpod-sdk-python/commit/5c02854efdc2874535d3d7867823048d5e8d4693))
+
+
+### Performance Improvements
+
+* **client:** optimize file structure copying in multipart requests ([cb792b6](https://github.com/gitpod-io/gitpod-sdk-python/commit/cb792b633104ff26eb799d8c32703920213ec23e))
+
+
+### Chores
+
+* **internal:** more robust bootstrap script ([5f05caa](https://github.com/gitpod-io/gitpod-sdk-python/commit/5f05caacfd1a55617d435845771e28503eff687c))
+
+
+### Documentation
+
+* **api:** update trigger usage note in AutomationTrigger ([5a292cb](https://github.com/gitpod-io/gitpod-sdk-python/commit/5a292cb1ef87a0dd73d93445707412d25c0e95e0))
+* **types:** mark is_admin deprecated in Organization model ([5e7b9f3](https://github.com/gitpod-io/gitpod-sdk-python/commit/5e7b9f3d7dd7af385ca75b17f01c1cab87da8cb6))
+
## 0.11.0 (2026-04-02)
Full Changelog: [v0.10.0...v0.11.0](https://github.com/gitpod-io/gitpod-sdk-python/compare/v0.10.0...v0.11.0)
diff --git a/api.md b/api.md
index dbd40b76..0bc97406 100644
--- a/api.md
+++ b/api.md
@@ -75,6 +75,7 @@ from gitpod.types import (
Role,
Type,
UserInputBlock,
+ UserInputMetadata,
WakeEvent,
AgentCreateExecutionConversationTokenResponse,
AgentCreatePromptResponse,
@@ -709,7 +710,7 @@ Methods:
- client.runners.check_repository_access(\*\*params) -> RunnerCheckRepositoryAccessResponse
- client.runners.create_logs_token(\*\*params) -> RunnerCreateLogsTokenResponse
- client.runners.create_runner_token(\*\*params) -> RunnerCreateRunnerTokenResponse
-- client.runners.list_scm_organizations(\*\*params) -> RunnerListScmOrganizationsResponse
+- client.runners.list_scm_organizations(\*\*params) -> SyncOrganizationsPage[RunnerListScmOrganizationsResponse]
- client.runners.parse_context_url(\*\*params) -> RunnerParseContextURLResponse
- client.runners.search_repositories(\*\*params) -> RunnerSearchRepositoriesResponse
diff --git a/pyproject.toml b/pyproject.toml
index 5d1671eb..0528316d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "gitpod-sdk"
-version = "0.11.0"
+version = "0.12.0"
description = "The official Python library for the gitpod API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/scripts/bootstrap b/scripts/bootstrap
index b430fee3..fe8451e4 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
echo -n "==> Install Homebrew dependencies? (y/N): "
read -r response
diff --git a/src/gitpod/_base_client.py b/src/gitpod/_base_client.py
index cc7f8af2..a9da58f9 100644
--- a/src/gitpod/_base_client.py
+++ b/src/gitpod/_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/gitpod/_files.py b/src/gitpod/_files.py
index cc14c14f..0fdce17b 100644
--- a/src/gitpod/_files.py
+++ b/src/gitpod/_files.py
@@ -3,8 +3,8 @@
import io
import os
import pathlib
-from typing import overload
-from typing_extensions import TypeGuard
+from typing import Sequence, cast, overload
+from typing_extensions import TypeVar, TypeGuard
import anyio
@@ -17,7 +17,9 @@
HttpxFileContent,
HttpxRequestFiles,
)
-from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
+from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t
+
+_T = TypeVar("_T")
def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
@@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
return await anyio.Path(file).read_bytes()
return file
+
+
+def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T:
+ """Copy only the containers along the given paths.
+
+ Used to guard against mutation by extract_files without copying the entire structure.
+ Only dicts and lists that lie on a path are copied; everything else
+ is returned by reference.
+
+ For example, given paths=[["foo", "files", "file"]] and the structure:
+ {
+ "foo": {
+ "bar": {"baz": {}},
+ "files": {"file": }
+ }
+ }
+ The root dict, "foo", and "files" are copied (they lie on the path).
+ "bar" and "baz" are returned by reference (off the path).
+ """
+ return _deepcopy_with_paths(item, paths, 0)
+
+
+def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T:
+ if not paths:
+ return item
+ if is_mapping(item):
+ key_to_paths: dict[str, list[Sequence[str]]] = {}
+ for path in paths:
+ if index < len(path):
+ key_to_paths.setdefault(path[index], []).append(path)
+
+ # if no path continues through this mapping, it won't be mutated and copying it is redundant
+ if not key_to_paths:
+ return item
+
+ result = dict(item)
+ for key, subpaths in key_to_paths.items():
+ if key in result:
+ result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1)
+ return cast(_T, result)
+ if is_list(item):
+ array_paths = [path for path in paths if index < len(path) and path[index] == ""]
+
+ # if no path expects a list here, nothing will be mutated inside it - return by reference
+ if not array_paths:
+ return cast(_T, item)
+ return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item])
+ return item
diff --git a/src/gitpod/_utils/__init__.py b/src/gitpod/_utils/__init__.py
index 10cb66d2..1c090e51 100644
--- a/src/gitpod/_utils/__init__.py
+++ b/src/gitpod/_utils/__init__.py
@@ -24,7 +24,6 @@
coerce_integer as coerce_integer,
file_from_path as file_from_path,
strip_not_given as strip_not_given,
- deepcopy_minimal as deepcopy_minimal,
get_async_library as get_async_library,
maybe_coerce_float as maybe_coerce_float,
get_required_header as get_required_header,
diff --git a/src/gitpod/_utils/_utils.py b/src/gitpod/_utils/_utils.py
index eec7f4a1..771859f5 100644
--- a/src/gitpod/_utils/_utils.py
+++ b/src/gitpod/_utils/_utils.py
@@ -86,8 +86,9 @@ def _extract_items(
index += 1
if is_dict(obj):
try:
- # We are at the last entry in the path so we must remove the field
- if (len(path)) == index:
+ # Remove the field if there are no more dict keys in the path,
+ # only "" traversal markers or end.
+ if all(p == "" for p in path[index:]):
item = obj.pop(key)
else:
item = obj[key]
@@ -176,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
return isinstance(obj, Iterable)
-def deepcopy_minimal(item: _T) -> _T:
- """Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
-
- - mappings, e.g. `dict`
- - list
-
- This is done for performance reasons.
- """
- if is_mapping(item):
- return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
- if is_list(item):
- return cast(_T, [deepcopy_minimal(entry) for entry in item])
- return item
-
-
# copied from https://github.com/Rapptz/RoboDanny
def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
size = len(seq)
diff --git a/src/gitpod/_version.py b/src/gitpod/_version.py
index 7504df3f..6521e93a 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.11.0" # x-release-please-version
+__version__ = "0.12.0" # x-release-please-version
diff --git a/src/gitpod/pagination.py b/src/gitpod/pagination.py
index b55ecc60..83771ae0 100644
--- a/src/gitpod/pagination.py
+++ b/src/gitpod/pagination.py
@@ -51,6 +51,9 @@
"MembersPagePagination",
"SyncMembersPage",
"AsyncMembersPage",
+ "OrganizationsPagePagination",
+ "SyncOrganizationsPage",
+ "AsyncOrganizationsPage",
"OutputsPagePagination",
"SyncOutputsPage",
"AsyncOutputsPage",
@@ -819,6 +822,56 @@ def next_page_info(self) -> Optional[PageInfo]:
return PageInfo(params={"token": next_token})
+class OrganizationsPagePagination(BaseModel):
+ next_token: Optional[str] = FieldInfo(alias="nextToken", default=None)
+
+
+class SyncOrganizationsPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
+ organizations: List[_T]
+ pagination: Optional[OrganizationsPagePagination] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ organizations = self.organizations
+ if not organizations:
+ return []
+ return organizations
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ next_token = None
+ if self.pagination is not None:
+ if self.pagination.next_token is not None:
+ next_token = self.pagination.next_token
+ if not next_token:
+ return None
+
+ return PageInfo(params={"token": next_token})
+
+
+class AsyncOrganizationsPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
+ organizations: List[_T]
+ pagination: Optional[OrganizationsPagePagination] = None
+
+ @override
+ def _get_page_items(self) -> List[_T]:
+ organizations = self.organizations
+ if not organizations:
+ return []
+ return organizations
+
+ @override
+ def next_page_info(self) -> Optional[PageInfo]:
+ next_token = None
+ if self.pagination is not None:
+ if self.pagination.next_token is not None:
+ next_token = self.pagination.next_token
+ if not next_token:
+ return None
+
+ return PageInfo(params={"token": next_token})
+
+
class OutputsPagePagination(BaseModel):
next_token: Optional[str] = FieldInfo(alias="nextToken", default=None)
diff --git a/src/gitpod/resources/runners/runners.py b/src/gitpod/resources/runners/runners.py
index 5a0ee2e4..1d92c061 100644
--- a/src/gitpod/resources/runners/runners.py
+++ b/src/gitpod/resources/runners/runners.py
@@ -41,7 +41,7 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from ...pagination import SyncRunnersPage, AsyncRunnersPage
+from ...pagination import SyncRunnersPage, AsyncRunnersPage, SyncOrganizationsPage, AsyncOrganizationsPage
from ..._base_client import AsyncPaginator, make_request_options
from ...types.runner import Runner
from ...types.runner_kind import RunnerKind
@@ -681,6 +681,8 @@ def list_scm_organizations(
*,
token: str | Omit = omit,
page_size: int | Omit = omit,
+ pagination: runner_list_scm_organizations_params.Pagination | Omit = omit,
+ query: str | 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.
@@ -689,7 +691,7 @@ def list_scm_organizations(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RunnerListScmOrganizationsResponse:
+ ) -> SyncOrganizationsPage[RunnerListScmOrganizationsResponse]:
"""
Lists SCM organizations the user belongs to.
@@ -709,7 +711,29 @@ def list_scm_organizations(
scmHost: "github.com"
```
+ - Search GitLab groups:
+
+ Returns the first page of GitLab groups matching the substring.
+
+ ```yaml
+ runnerId: "d2c94c27-3b76-4a42-b88c-95a85e392c68"
+ scmHost: "gitlab.com"
+ query: "platform"
+ pagination:
+ pageSize: 25
+ ```
+
Args:
+ pagination: Pagination parameters. When unset, defaults to the standard PaginationRequest
+ defaults (page_size 25, max 100). Tokens are opaque and provider-specific.
+
+ query: Optional substring filter applied to the organization name.
+
+ - GitLab: forwarded to the upstream `search` parameter (server-side,
+ case-insensitive substring on name/path).
+ - GitHub and Bitbucket: not implemented as they don't support searching Empty
+ value means no filter.
+
scm_host: The SCM host to list organizations from (e.g., "github.com", "gitlab.com")
extra_headers: Send extra headers
@@ -720,10 +744,13 @@ def list_scm_organizations(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._post(
+ return self._get_api_list(
"/gitpod.v1.RunnerService/ListSCMOrganizations",
+ page=SyncOrganizationsPage[RunnerListScmOrganizationsResponse],
body=maybe_transform(
{
+ "pagination": pagination,
+ "query": query,
"runner_id": runner_id,
"scm_host": scm_host,
},
@@ -742,7 +769,8 @@ def list_scm_organizations(
runner_list_scm_organizations_params.RunnerListScmOrganizationsParams,
),
),
- cast_to=RunnerListScmOrganizationsResponse,
+ model=RunnerListScmOrganizationsResponse,
+ method="post",
)
def parse_context_url(
@@ -1505,11 +1533,13 @@ async def create_runner_token(
cast_to=RunnerCreateRunnerTokenResponse,
)
- async def list_scm_organizations(
+ def list_scm_organizations(
self,
*,
token: str | Omit = omit,
page_size: int | Omit = omit,
+ pagination: runner_list_scm_organizations_params.Pagination | Omit = omit,
+ query: str | 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.
@@ -1518,7 +1548,7 @@ async def list_scm_organizations(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> RunnerListScmOrganizationsResponse:
+ ) -> AsyncPaginator[RunnerListScmOrganizationsResponse, AsyncOrganizationsPage[RunnerListScmOrganizationsResponse]]:
"""
Lists SCM organizations the user belongs to.
@@ -1538,7 +1568,29 @@ async def list_scm_organizations(
scmHost: "github.com"
```
+ - Search GitLab groups:
+
+ Returns the first page of GitLab groups matching the substring.
+
+ ```yaml
+ runnerId: "d2c94c27-3b76-4a42-b88c-95a85e392c68"
+ scmHost: "gitlab.com"
+ query: "platform"
+ pagination:
+ pageSize: 25
+ ```
+
Args:
+ pagination: Pagination parameters. When unset, defaults to the standard PaginationRequest
+ defaults (page_size 25, max 100). Tokens are opaque and provider-specific.
+
+ query: Optional substring filter applied to the organization name.
+
+ - GitLab: forwarded to the upstream `search` parameter (server-side,
+ case-insensitive substring on name/path).
+ - GitHub and Bitbucket: not implemented as they don't support searching Empty
+ value means no filter.
+
scm_host: The SCM host to list organizations from (e.g., "github.com", "gitlab.com")
extra_headers: Send extra headers
@@ -1549,10 +1601,13 @@ async def list_scm_organizations(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return await self._post(
+ return self._get_api_list(
"/gitpod.v1.RunnerService/ListSCMOrganizations",
- body=await async_maybe_transform(
+ page=AsyncOrganizationsPage[RunnerListScmOrganizationsResponse],
+ body=maybe_transform(
{
+ "pagination": pagination,
+ "query": query,
"runner_id": runner_id,
"scm_host": scm_host,
},
@@ -1563,7 +1618,7 @@ async def list_scm_organizations(
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=await async_maybe_transform(
+ query=maybe_transform(
{
"token": token,
"page_size": page_size,
@@ -1571,7 +1626,8 @@ async def list_scm_organizations(
runner_list_scm_organizations_params.RunnerListScmOrganizationsParams,
),
),
- cast_to=RunnerListScmOrganizationsResponse,
+ model=RunnerListScmOrganizationsResponse,
+ method="post",
)
async def parse_context_url(
diff --git a/src/gitpod/types/agent_execution.py b/src/gitpod/types/agent_execution.py
index 07f02eda..102b08ca 100644
--- a/src/gitpod/types/agent_execution.py
+++ b/src/gitpod/types/agent_execution.py
@@ -455,11 +455,13 @@ class Status(BaseModel):
"SUPPORTED_MODEL_OPUS_4_5_EXTENDED",
"SUPPORTED_MODEL_OPUS_4_6",
"SUPPORTED_MODEL_OPUS_4_6_EXTENDED",
+ "SUPPORTED_MODEL_OPUS_4_7",
"SUPPORTED_MODEL_HAIKU_4_5",
"SUPPORTED_MODEL_OPENAI_4O",
"SUPPORTED_MODEL_OPENAI_4O_MINI",
"SUPPORTED_MODEL_OPENAI_O1",
"SUPPORTED_MODEL_OPENAI_O1_MINI",
+ "SUPPORTED_MODEL_OPENAI_AUTO",
]
] = FieldInfo(alias="supportedModel", default=None)
"""supported_model is the LLM model being used by the agent execution."""
diff --git a/src/gitpod/types/environment_status.py b/src/gitpod/types/environment_status.py
index 45ad12ca..d5ed5c65 100644
--- a/src/gitpod/types/environment_status.py
+++ b/src/gitpod/types/environment_status.py
@@ -91,6 +91,12 @@ class ContentGitChangedFile(BaseModel):
] = FieldInfo(alias="changeType", default=None)
"""ChangeType is the type of change that happened to the file"""
+ old_path: Optional[str] = FieldInfo(alias="oldPath", default=None)
+ """
+ old_path is the previous path of the file before a rename or copy. Only set when
+ change_type is RENAMED or COPIED.
+ """
+
path: Optional[str] = None
"""path is the path of the file"""
diff --git a/src/gitpod/types/environments/automations_file_param.py b/src/gitpod/types/environments/automations_file_param.py
index 226782ce..68deffa5 100644
--- a/src/gitpod/types/environments/automations_file_param.py
+++ b/src/gitpod/types/environments/automations_file_param.py
@@ -54,7 +54,8 @@ class Services(TypedDict, total=False):
runs_on: Annotated[RunsOn, PropertyInfo(alias="runsOn")]
triggered_by: Annotated[
- List[Literal["manual", "postEnvironmentStart", "postDevcontainerStart"]], PropertyInfo(alias="triggeredBy")
+ List[Literal["manual", "postEnvironmentStart", "postDevcontainerStart", "prebuild"]],
+ PropertyInfo(alias="triggeredBy"),
]
diff --git a/src/gitpod/types/runner_capability.py b/src/gitpod/types/runner_capability.py
index c2e50b46..2e473ac1 100644
--- a/src/gitpod/types/runner_capability.py
+++ b/src/gitpod/types/runner_capability.py
@@ -18,4 +18,5 @@
"RUNNER_CAPABILITY_RUNNER_SIDE_AGENT",
"RUNNER_CAPABILITY_WARM_POOL",
"RUNNER_CAPABILITY_ASG_WARM_POOL",
+ "RUNNER_CAPABILITY_PORT_AUTHENTICATION",
]
diff --git a/src/gitpod/types/runner_list_scm_organizations_params.py b/src/gitpod/types/runner_list_scm_organizations_params.py
index bd1f788b..a852d57b 100644
--- a/src/gitpod/types/runner_list_scm_organizations_params.py
+++ b/src/gitpod/types/runner_list_scm_organizations_params.py
@@ -6,7 +6,7 @@
from .._utils import PropertyInfo
-__all__ = ["RunnerListScmOrganizationsParams"]
+__all__ = ["RunnerListScmOrganizationsParams", "Pagination"]
class RunnerListScmOrganizationsParams(TypedDict, total=False):
@@ -14,7 +14,43 @@ class RunnerListScmOrganizationsParams(TypedDict, total=False):
page_size: Annotated[int, PropertyInfo(alias="pageSize")]
+ pagination: Pagination
+ """Pagination parameters.
+
+ When unset, defaults to the standard PaginationRequest defaults (page_size 25,
+ max 100). Tokens are opaque and provider-specific.
+ """
+
+ query: str
+ """Optional substring filter applied to the organization name.
+
+ - GitLab: forwarded to the upstream `search` parameter (server-side,
+ case-insensitive substring on name/path).
+ - GitHub and Bitbucket: not implemented as they don't support searching Empty
+ value means no filter.
+ """
+
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")"""
+
+
+class Pagination(TypedDict, total=False):
+ """Pagination parameters.
+
+ When unset, defaults to the standard PaginationRequest defaults
+ (page_size 25, max 100). Tokens are opaque and provider-specific.
+ """
+
+ token: str
+ """
+ Token for the next set of results that was returned as next_token of a
+ PaginationResponse
+ """
+
+ page_size: Annotated[int, PropertyInfo(alias="pageSize")]
+ """Page size is the maximum number of results to retrieve per page. Defaults to 25.
+
+ Maximum 100.
+ """
diff --git a/src/gitpod/types/runner_list_scm_organizations_response.py b/src/gitpod/types/runner_list_scm_organizations_response.py
index 8e9db077..8c8c50e8 100644
--- a/src/gitpod/types/runner_list_scm_organizations_response.py
+++ b/src/gitpod/types/runner_list_scm_organizations_response.py
@@ -1,19 +1,24 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import Optional
from pydantic import Field as FieldInfo
from .._models import BaseModel
-__all__ = ["RunnerListScmOrganizationsResponse", "Organization"]
+__all__ = ["RunnerListScmOrganizationsResponse"]
-class Organization(BaseModel):
+class RunnerListScmOrganizationsResponse(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.
+ Deprecated: this field is unused by all known consumers and is scheduled for
+ removal in a future release. Do not read it.
+
+ Originally intended to gate organization-level webhook creation in the
+ dashboard, but that gating was never implemented. Populating this field on the
+ GitLab path requires a second fully-paginated ListGroups call, which is the main
+ reason we are deprecating it.
"""
name: Optional[str] = None
@@ -21,8 +26,3 @@ class Organization(BaseModel):
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/shared/automation_trigger.py b/src/gitpod/types/shared/automation_trigger.py
index 8ffd732b..3d1fafb8 100644
--- a/src/gitpod/types/shared/automation_trigger.py
+++ b/src/gitpod/types/shared/automation_trigger.py
@@ -20,7 +20,7 @@ class AutomationTrigger(BaseModel):
The `prebuild` field starts the automation during a prebuild of an environment. This phase does not have user secrets available.
The `before_snapshot` field triggers the automation after all prebuild tasks complete but before the snapshot is taken.
This is used for tasks that need to run last during prebuilds, such as IDE warmup.
- Note: The prebuild and before_snapshot triggers can only be used with tasks, not services.
+ Note: The before_snapshot trigger can only be used with tasks, not services.
"""
before_snapshot: Optional[bool] = FieldInfo(alias="beforeSnapshot", default=None)
diff --git a/src/gitpod/types/shared/runs_on.py b/src/gitpod/types/shared/runs_on.py
index 5f26ef47..33cf1bd5 100644
--- a/src/gitpod/types/shared/runs_on.py
+++ b/src/gitpod/types/shared/runs_on.py
@@ -18,9 +18,3 @@ class RunsOn(BaseModel):
machine: Optional[object] = None
"""Machine runs the service/task directly on the VM/machine level."""
-
- terminal: Optional[object] = None
- """
- Terminal runs the service inside a managed PTY terminal in the devcontainer.
- Users can attach to the terminal interactively via the terminal API.
- """
diff --git a/src/gitpod/types/shared_params/automation_trigger.py b/src/gitpod/types/shared_params/automation_trigger.py
index 27dc462b..1020f005 100644
--- a/src/gitpod/types/shared_params/automation_trigger.py
+++ b/src/gitpod/types/shared_params/automation_trigger.py
@@ -20,7 +20,7 @@ class AutomationTrigger(TypedDict, total=False):
The `prebuild` field starts the automation during a prebuild of an environment. This phase does not have user secrets available.
The `before_snapshot` field triggers the automation after all prebuild tasks complete but before the snapshot is taken.
This is used for tasks that need to run last during prebuilds, such as IDE warmup.
- Note: The prebuild and before_snapshot triggers can only be used with tasks, not services.
+ Note: The before_snapshot trigger can only be used with tasks, not services.
"""
before_snapshot: Annotated[bool, PropertyInfo(alias="beforeSnapshot")]
diff --git a/src/gitpod/types/shared_params/runs_on.py b/src/gitpod/types/shared_params/runs_on.py
index 0182710a..8fc0fd3c 100644
--- a/src/gitpod/types/shared_params/runs_on.py
+++ b/src/gitpod/types/shared_params/runs_on.py
@@ -20,9 +20,3 @@ class RunsOn(TypedDict, total=False):
machine: object
"""Machine runs the service/task directly on the VM/machine level."""
-
- terminal: object
- """
- Terminal runs the service inside a managed PTY terminal in the devcontainer.
- Users can attach to the terminal interactively via the terminal API.
- """
diff --git a/src/gitpod/types/workflow_trigger.py b/src/gitpod/types/workflow_trigger.py
index 4b044761..cefa42fc 100644
--- a/src/gitpod/types/workflow_trigger.py
+++ b/src/gitpod/types/workflow_trigger.py
@@ -27,10 +27,18 @@ class PullRequest(BaseModel):
"PULL_REQUEST_EVENT_MERGED",
"PULL_REQUEST_EVENT_CLOSED",
"PULL_REQUEST_EVENT_READY_FOR_REVIEW",
+ "PULL_REQUEST_EVENT_REVIEW_REQUESTED",
]
]
] = None
+ integration_id: Optional[str] = FieldInfo(alias="integrationId", default=None)
+ """
+ integration_id is the optional ID of an integration that acts as the source of
+ webhook events. When set, the trigger will be activated when the webhook
+ receives events.
+ """
+
webhook_id: Optional[str] = FieldInfo(alias="webhookId", default=None)
"""
webhook_id is the optional ID of a webhook that this trigger is bound to. When
diff --git a/src/gitpod/types/workflow_trigger_param.py b/src/gitpod/types/workflow_trigger_param.py
index ed28c9f8..ee1ef3c1 100644
--- a/src/gitpod/types/workflow_trigger_param.py
+++ b/src/gitpod/types/workflow_trigger_param.py
@@ -26,9 +26,17 @@ class PullRequest(TypedDict, total=False):
"PULL_REQUEST_EVENT_MERGED",
"PULL_REQUEST_EVENT_CLOSED",
"PULL_REQUEST_EVENT_READY_FOR_REVIEW",
+ "PULL_REQUEST_EVENT_REVIEW_REQUESTED",
]
]
+ integration_id: Annotated[Optional[str], PropertyInfo(alias="integrationId")]
+ """
+ integration_id is the optional ID of an integration that acts as the source of
+ webhook events. When set, the trigger will be activated when the webhook
+ receives events.
+ """
+
webhook_id: Annotated[Optional[str], PropertyInfo(alias="webhookId")]
"""
webhook_id is the optional ID of a webhook that this trigger is bound to. When
diff --git a/tests/api_resources/environments/automations/test_services.py b/tests/api_resources/environments/automations/test_services.py
index 3a80cc01..3d1ccf73 100644
--- a/tests/api_resources/environments/automations/test_services.py
+++ b/tests/api_resources/environments/automations/test_services.py
@@ -75,7 +75,6 @@ def test_method_create_with_all_params(self, client: Gitpod) -> None:
"image": "x",
},
"machine": {},
- "terminal": {},
},
"session": "session",
"spec_version": "specVersion",
@@ -188,7 +187,6 @@ def test_method_update_with_all_params(self, client: Gitpod) -> None:
"image": "x",
},
"machine": {},
- "terminal": {},
},
},
status={
@@ -437,7 +435,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncGitpod) ->
"image": "x",
},
"machine": {},
- "terminal": {},
},
"session": "session",
"spec_version": "specVersion",
@@ -550,7 +547,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncGitpod) ->
"image": "x",
},
"machine": {},
- "terminal": {},
},
},
status={
diff --git a/tests/api_resources/environments/automations/test_tasks.py b/tests/api_resources/environments/automations/test_tasks.py
index 29599ec1..242dc25e 100644
--- a/tests/api_resources/environments/automations/test_tasks.py
+++ b/tests/api_resources/environments/automations/test_tasks.py
@@ -71,7 +71,6 @@ def test_method_create_with_all_params(self, client: Gitpod) -> None:
"image": "x",
},
"machine": {},
- "terminal": {},
},
},
)
@@ -178,7 +177,6 @@ def test_method_update_with_all_params(self, client: Gitpod) -> None:
"image": "x",
},
"machine": {},
- "terminal": {},
},
},
)
@@ -377,7 +375,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncGitpod) ->
"image": "x",
},
"machine": {},
- "terminal": {},
},
},
)
@@ -484,7 +481,6 @@ async def test_method_update_with_all_params(self, async_client: AsyncGitpod) ->
"image": "x",
},
"machine": {},
- "terminal": {},
},
},
)
diff --git a/tests/api_resources/environments/test_automations.py b/tests/api_resources/environments/test_automations.py
index bed1aa5a..15f3fe3a 100644
--- a/tests/api_resources/environments/test_automations.py
+++ b/tests/api_resources/environments/test_automations.py
@@ -44,7 +44,6 @@ def test_method_upsert_with_all_params(self, client: Gitpod) -> None:
"image": "x",
},
"machine": {},
- "terminal": {},
},
"triggered_by": ["postDevcontainerStart"],
}
@@ -61,7 +60,6 @@ def test_method_upsert_with_all_params(self, client: Gitpod) -> None:
"image": "x",
},
"machine": {},
- "terminal": {},
},
"triggered_by": ["postEnvironmentStart"],
}
@@ -126,7 +124,6 @@ async def test_method_upsert_with_all_params(self, async_client: AsyncGitpod) ->
"image": "x",
},
"machine": {},
- "terminal": {},
},
"triggered_by": ["postDevcontainerStart"],
}
@@ -143,7 +140,6 @@ async def test_method_upsert_with_all_params(self, async_client: AsyncGitpod) ->
"image": "x",
},
"machine": {},
- "terminal": {},
},
"triggered_by": ["postEnvironmentStart"],
}
diff --git a/tests/api_resources/test_automations.py b/tests/api_resources/test_automations.py
index f1412921..943c0d0a 100644
--- a/tests/api_resources/test_automations.py
+++ b/tests/api_resources/test_automations.py
@@ -113,6 +113,7 @@ def test_method_create_with_all_params(self, client: Gitpod) -> None:
"manual": {},
"pull_request": {
"events": ["PULL_REQUEST_EVENT_UNSPECIFIED"],
+ "integration_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
"webhook_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
},
"time": {"cron_expression": "cronExpression"},
@@ -256,6 +257,7 @@ def test_method_update_with_all_params(self, client: Gitpod) -> None:
"manual": {},
"pull_request": {
"events": ["PULL_REQUEST_EVENT_UNSPECIFIED"],
+ "integration_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
"webhook_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
},
"time": {"cron_expression": "cronExpression"},
@@ -793,6 +795,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncGitpod) ->
"manual": {},
"pull_request": {
"events": ["PULL_REQUEST_EVENT_UNSPECIFIED"],
+ "integration_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
"webhook_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
},
"time": {"cron_expression": "cronExpression"},
@@ -936,6 +939,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncGitpod) ->
"manual": {},
"pull_request": {
"events": ["PULL_REQUEST_EVENT_UNSPECIFIED"],
+ "integration_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
"webhook_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
},
"time": {"cron_expression": "cronExpression"},
diff --git a/tests/api_resources/test_runners.py b/tests/api_resources/test_runners.py
index 3a7c4506..dad788c8 100644
--- a/tests/api_resources/test_runners.py
+++ b/tests/api_resources/test_runners.py
@@ -21,7 +21,7 @@
RunnerCheckRepositoryAccessResponse,
RunnerCheckAuthenticationForHostResponse,
)
-from gitpod.pagination import SyncRunnersPage, AsyncRunnersPage
+from gitpod.pagination import SyncRunnersPage, AsyncRunnersPage, SyncOrganizationsPage, AsyncOrganizationsPage
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -416,7 +416,7 @@ def test_streaming_response_create_runner_token(self, client: Gitpod) -> None:
@parametrize
def test_method_list_scm_organizations(self, client: Gitpod) -> None:
runner = client.runners.list_scm_organizations()
- assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"])
+ assert_matches_type(SyncOrganizationsPage[RunnerListScmOrganizationsResponse], runner, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -424,10 +424,15 @@ def test_method_list_scm_organizations_with_all_params(self, client: Gitpod) ->
runner = client.runners.list_scm_organizations(
token="token",
page_size=0,
+ pagination={
+ "token": "token",
+ "page_size": 100,
+ },
+ query="query",
runner_id="d2c94c27-3b76-4a42-b88c-95a85e392c68",
scm_host="github.com",
)
- assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"])
+ assert_matches_type(SyncOrganizationsPage[RunnerListScmOrganizationsResponse], runner, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -437,7 +442,7 @@ def test_raw_response_list_scm_organizations(self, client: Gitpod) -> None:
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"])
+ assert_matches_type(SyncOrganizationsPage[RunnerListScmOrganizationsResponse], runner, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -447,7 +452,7 @@ def test_streaming_response_list_scm_organizations(self, client: Gitpod) -> None
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
runner = response.parse()
- assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"])
+ assert_matches_type(SyncOrganizationsPage[RunnerListScmOrganizationsResponse], runner, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -925,7 +930,7 @@ async def test_streaming_response_create_runner_token(self, async_client: AsyncG
@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"])
+ assert_matches_type(AsyncOrganizationsPage[RunnerListScmOrganizationsResponse], runner, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -933,10 +938,15 @@ async def test_method_list_scm_organizations_with_all_params(self, async_client:
runner = await async_client.runners.list_scm_organizations(
token="token",
page_size=0,
+ pagination={
+ "token": "token",
+ "page_size": 100,
+ },
+ query="query",
runner_id="d2c94c27-3b76-4a42-b88c-95a85e392c68",
scm_host="github.com",
)
- assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"])
+ assert_matches_type(AsyncOrganizationsPage[RunnerListScmOrganizationsResponse], runner, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -946,7 +956,7 @@ async def test_raw_response_list_scm_organizations(self, async_client: AsyncGitp
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"])
+ assert_matches_type(AsyncOrganizationsPage[RunnerListScmOrganizationsResponse], runner, path=["response"])
@pytest.mark.skip(reason="Mock server tests are disabled")
@parametrize
@@ -956,7 +966,7 @@ async def test_streaming_response_list_scm_organizations(self, async_client: Asy
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
runner = await response.parse()
- assert_matches_type(RunnerListScmOrganizationsResponse, runner, path=["response"])
+ assert_matches_type(AsyncOrganizationsPage[RunnerListScmOrganizationsResponse], runner, path=["response"])
assert cast(Any, response.is_closed) is True
diff --git a/tests/test_client.py b/tests/test_client.py
index 92b0eea3..c105b4d9 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -438,6 +438,30 @@ def test_default_query_option(self) -> None:
client.close()
+ def test_hardcoded_query_params_in_url(self, client: Gitpod) -> 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: Gitpod) -> None:
request = client._build_request(
FinalRequestOptions(
@@ -1363,6 +1387,30 @@ async def test_default_query_option(self) -> None:
await client.close()
+ async def test_hardcoded_query_params_in_url(self, async_client: AsyncGitpod) -> 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: Gitpod) -> None:
request = client._build_request(
FinalRequestOptions(
diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py
deleted file mode 100644
index c498f531..00000000
--- a/tests/test_deepcopy.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from gitpod._utils import deepcopy_minimal
-
-
-def assert_different_identities(obj1: object, obj2: object) -> None:
- assert obj1 == obj2
- assert id(obj1) != id(obj2)
-
-
-def test_simple_dict() -> None:
- obj1 = {"foo": "bar"}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_dict() -> None:
- obj1 = {"foo": {"bar": True}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
-
-
-def test_complex_nested_dict() -> None:
- obj1 = {"foo": {"bar": [{"hello": "world"}]}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
- assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"])
- assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0])
-
-
-def test_simple_list() -> None:
- obj1 = ["a", "b", "c"]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_list() -> None:
- obj1 = ["a", [1, 2, 3]]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1[1], obj2[1])
-
-
-class MyObject: ...
-
-
-def test_ignores_other_types() -> None:
- # custom classes
- my_obj = MyObject()
- obj1 = {"foo": my_obj}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert obj1["foo"] is my_obj
-
- # tuples
- obj3 = ("a", "b")
- obj4 = deepcopy_minimal(obj3)
- assert obj3 is obj4
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index 0ca5a8dc..a9d8b167 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -35,6 +35,15 @@ def test_multiple_files() -> None:
assert query == {"documents": [{}, {}]}
+def test_top_level_file_array() -> None:
+ query = {"files": [b"file one", b"file two"], "title": "hello"}
+ assert extract_files(query, paths=[["files", ""]]) == [
+ ("files[]", b"file one"),
+ ("files[]", b"file two"),
+ ]
+ assert query == {"title": "hello"}
+
+
@pytest.mark.parametrize(
"query,paths,expected",
[
diff --git a/tests/test_files.py b/tests/test_files.py
index efde0d4e..2e09e8a4 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -4,7 +4,8 @@
import pytest
from dirty_equals import IsDict, IsList, IsBytes, IsTuple
-from gitpod._files import to_httpx_files, async_to_httpx_files
+from gitpod._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files
+from gitpod._utils import extract_files
readme_path = Path(__file__).parent.parent.joinpath("README.md")
@@ -49,3 +50,99 @@ def test_string_not_allowed() -> None:
"file": "foo", # type: ignore
}
)
+
+
+def assert_different_identities(obj1: object, obj2: object) -> None:
+ assert obj1 == obj2
+ assert obj1 is not obj2
+
+
+class TestDeepcopyWithPaths:
+ def test_copies_top_level_dict(self) -> None:
+ original = {"file": b"data", "other": "value"}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+
+ def test_file_value_is_same_reference(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+ assert result["file"] is file_bytes
+
+ def test_list_popped_wholesale(self) -> None:
+ files = [b"f1", b"f2"]
+ original = {"files": files, "title": "t"}
+ result = deepcopy_with_paths(original, [["files", ""]])
+ assert_different_identities(result, original)
+ result_files = result["files"]
+ assert isinstance(result_files, list)
+ assert_different_identities(result_files, files)
+
+ def test_nested_array_path_copies_list_and_elements(self) -> None:
+ elem1 = {"file": b"f1", "extra": 1}
+ elem2 = {"file": b"f2", "extra": 2}
+ original = {"items": [elem1, elem2]}
+ result = deepcopy_with_paths(original, [["items", "", "file"]])
+ assert_different_identities(result, original)
+ result_items = result["items"]
+ assert isinstance(result_items, list)
+ assert_different_identities(result_items, original["items"])
+ assert_different_identities(result_items[0], elem1)
+ assert_different_identities(result_items[1], elem2)
+
+ def test_empty_paths_returns_same_object(self) -> None:
+ original = {"foo": "bar"}
+ result = deepcopy_with_paths(original, [])
+ assert result is original
+
+ def test_multiple_paths(self) -> None:
+ f1 = b"file1"
+ f2 = b"file2"
+ original = {"a": f1, "b": f2, "c": "unchanged"}
+ result = deepcopy_with_paths(original, [["a"], ["b"]])
+ assert_different_identities(result, original)
+ assert result["a"] is f1
+ assert result["b"] is f2
+ assert result["c"] is original["c"]
+
+ def test_extract_files_does_not_mutate_original_top_level(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes, "other": "value"}
+
+ copied = deepcopy_with_paths(original, [["file"]])
+ extracted = extract_files(copied, paths=[["file"]])
+
+ assert extracted == [("file", file_bytes)]
+ assert original == {"file": file_bytes, "other": "value"}
+ assert copied == {"other": "value"}
+
+ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None:
+ file1 = b"f1"
+ file2 = b"f2"
+ original = {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+
+ copied = deepcopy_with_paths(original, [["items", "", "file"]])
+ extracted = extract_files(copied, paths=[["items", "", "file"]])
+
+ assert extracted == [("items[][file]", file1), ("items[][file]", file2)]
+ assert original == {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+ assert copied == {
+ "items": [
+ {"extra": 1},
+ {"extra": 2},
+ ],
+ "title": "example",
+ }