From 124186132984eb5208c59f29a49982efa9191713 Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Mon, 10 Aug 2026 10:11:01 -0700 Subject: [PATCH] feat: add environment files support and scotty file download helper PiperOrigin-RevId: 962222280 --- google/genai/_gaos/environments.py | 42 ++ google/genai/_gaos/files.py | 403 ++++++++++++++++++ google/genai/_gaos/google_genai.py | 107 ++++- google/genai/_gaos/models/__init__.py | 14 + .../genai/_gaos/models/getenvironmentfiles.py | 116 +++++ .../_gaos/resources/environments/__init__.py | 6 + .../_gaos/types/environments/__init__.py | 15 + .../types/environments/environmentfile.py | 117 +++++ .../getenvironmentfilesresponse.py | 71 +++ .../tests/gaos/test_environments_lifecycle.py | 260 +++++++++++ 10 files changed, 1143 insertions(+), 8 deletions(-) create mode 100644 google/genai/_gaos/files.py create mode 100644 google/genai/_gaos/models/getenvironmentfiles.py create mode 100644 google/genai/_gaos/types/environments/environmentfile.py create mode 100644 google/genai/_gaos/types/environments/getenvironmentfilesresponse.py diff --git a/google/genai/_gaos/environments.py b/google/genai/_gaos/environments.py index 63debf44e..a7843110d 100644 --- a/google/genai/_gaos/environments.py +++ b/google/genai/_gaos/environments.py @@ -19,6 +19,8 @@ from . import errors, models, types, utils from ._hooks import AfterParseErrorContext, HookContext, ResponseContext from .basesdk import AsyncBaseSDK, BaseSDK +from .files import AsyncFiles, Files +from .sdkconfiguration import SDKConfiguration from .types import OptionalNullable, UNSET, environments, interactions from .types.environments import ( createenvironmentrequest as environments_createenvironmentrequest, @@ -39,6 +41,18 @@ def with_raw_response(self): def with_streaming_response(self): return EnvironmentsWithStreamingResponse(self) + files: Files + + def __init__( + self, sdk_config: SDKConfiguration, parent_ref: Optional[object] = None + ) -> None: + BaseSDK.__init__(self, sdk_config, parent_ref=parent_ref) + self.sdk_configuration = sdk_config + self._init_sdks() + + def _init_sdks(self): + self.files = Files(self.sdk_configuration, parent_ref=self.parent_ref) + def create_environment( self, *, @@ -688,6 +702,10 @@ def __init__(self, sdk: Environments) -> None: sdk.delete_environment, "extra_headers" ) + @property + def files(self): + return self._sdk.files.with_raw_response + class EnvironmentsWithStreamingResponse: def __init__(self, sdk: Environments) -> None: @@ -705,6 +723,10 @@ def __init__(self, sdk: Environments) -> None: sdk.delete_environment, "extra_headers" ) + @property + def files(self): + return self._sdk.files.with_streaming_response + class AsyncEnvironments(AsyncBaseSDK): @property @@ -715,6 +737,18 @@ def with_raw_response(self): def with_streaming_response(self): return AsyncEnvironmentsWithStreamingResponse(self) + files: AsyncFiles + + def __init__( + self, sdk_config: SDKConfiguration, parent_ref: Optional[object] = None + ) -> None: + AsyncBaseSDK.__init__(self, sdk_config, parent_ref=parent_ref) + self.sdk_configuration = sdk_config + self._init_sdks() + + def _init_sdks(self): + self.files = AsyncFiles(self.sdk_configuration, parent_ref=self.parent_ref) + async def create_environment( self, *, @@ -1376,6 +1410,10 @@ def __init__(self, sdk: AsyncEnvironments) -> None: sdk.delete_environment, "extra_headers" ) + @property + def files(self): + return self._sdk.files.with_raw_response + class AsyncEnvironmentsWithStreamingResponse: def __init__(self, sdk: AsyncEnvironments) -> None: @@ -1392,3 +1430,7 @@ def __init__(self, sdk: AsyncEnvironments) -> None: self.delete_environment = response_helpers.async_to_streamed_response_wrapper( sdk.delete_environment, "extra_headers" ) + + @property + def files(self): + return self._sdk.files.with_streaming_response diff --git a/google/genai/_gaos/files.py b/google/genai/_gaos/files.py new file mode 100644 index 000000000..f4e7c428f --- /dev/null +++ b/google/genai/_gaos/files.py @@ -0,0 +1,403 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pyformat: disable + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from . import errors, models, types, utils +from ._hooks import AfterParseErrorContext, HookContext, ResponseContext +from .basesdk import AsyncBaseSDK, BaseSDK +from .types import OptionalNullable, UNSET, environments +from .utils import get_security_from_env, response_helpers +from .utils.unmarshal_json_response import unmarshal_json_response +import httpx +from typing import Any, Mapping, Optional, Union, cast + + +class Files(BaseSDK): + @property + def with_raw_response(self): + return FilesWithRawResponse(self) + + @property + def with_streaming_response(self): + return FilesWithStreamingResponse(self) + + def get( + self, + environment: str, + path: str, + *, + api_version: Optional[str] = None, + page_size: Optional[int] = None, + page_token: Optional[str] = None, + recursive: Optional[bool] = None, + extra_headers: Optional[Mapping[str, str]] = None, + extra_query: Optional[Mapping[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + ) -> environments.GetEnvironmentFilesResponse: + r"""Retrieves a file or directory from an environment's snapshot. + + :param environment: + :param path: + :param api_version: Which version of the API to use. + :param page_size: Optional. Maximum number of entries to return per page (for directory listing). + :param page_token: Optional. Pagination token for directory listing. + :param recursive: Optional. If true and the path is a directory, recursively lists all files. + :param extra_headers: Additional headers to set or replace on requests. + :param extra_query: Additional query parameters to append to requests. + :param timeout: Override the default request timeout configuration for this method in seconds + """ + base_url = None + url_variables = None + retries: OptionalNullable[utils.RetryConfig] = UNSET + server_url = None + http_headers = extra_headers + timeout_ms = self._coerce_timeout_ms(timeout) + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.GetEnvironmentFilesRequest( + api_version=api_version, + environment=environment, + path=path, + page_size=page_size, + page_token=page_token, + recursive=recursive, + ) + + _speakeasy_response_mode, http_headers = response_helpers.consume_response_mode( + http_headers + ) + req = self._build_request( + method="GET", + path="/{api_version}/environments/{environment}/files/{path}", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + extra_query_params=extra_query, + _globals=models.GetEnvironmentFilesGlobals( + api_version=self.sdk_configuration.globals.api_version, + ), + security=self.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "attempt-count-backoff", + utils.BackoffStrategy(500, 8000, 2, 30000), + True, + max_retries=4, + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["408", "409", "429", "5XX"]) + + def _speakeasy_parse_response(http_res): + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response( + environments.GetEnvironmentFilesResponse, http_res, validate=False + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.GenAiDefaultError( + "API error occurred", http_res, http_res_text + ) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.GenAiDefaultError( + "API error occurred", http_res, http_res_text + ) + + raise errors.GenAiDefaultError("Unexpected response received", http_res) + + _speakeasy_hook_ctx = HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="GetEnvironmentFiles", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, types.Security + ), + tags=None, + extensions=None, + response=ResponseContext(mode=_speakeasy_response_mode, execution="sync"), + ) + http_res = self.do_request( + hook_ctx=_speakeasy_hook_ctx, + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + stream=_speakeasy_response_mode == "streaming", + retry_config=retry_config, + ) + if _speakeasy_response_mode != "parsed": + if utils.match_status_codes(["4XX", "5XX"], http_res.status_code): + http_res.read() + try: + _speakeasy_parse_response(http_res) + except Exception as parse_exc_: + response_helpers.raise_parse_error( + self.sdk_configuration.__dict__["_hooks"], + AfterParseErrorContext(_speakeasy_hook_ctx), + http_res, + parse_exc_, + ) + _speakeasy_response_cls = ( + response_helpers.StreamedAPIResponse + if _speakeasy_response_mode == "streaming" + else response_helpers.APIResponse + ) + return cast( + Any, + _speakeasy_response_cls( + raw=http_res, + parser=_speakeasy_parse_response, + mode="buffered", + client_ref=self, + hook_ctx=AfterParseErrorContext(_speakeasy_hook_ctx), + hooks=self.sdk_configuration.__dict__.get("_hooks"), + ), + ) + try: + return _speakeasy_parse_response(http_res) + except Exception as parse_exc_: + response_helpers.raise_parse_error( + self.sdk_configuration.__dict__["_hooks"], + AfterParseErrorContext(_speakeasy_hook_ctx), + http_res, + parse_exc_, + ) + + +class FilesWithRawResponse: + def __init__(self, sdk: Files) -> None: + self._sdk = sdk + self.get = response_helpers.to_raw_response_wrapper(sdk.get, "extra_headers") + + +class FilesWithStreamingResponse: + def __init__(self, sdk: Files) -> None: + self._sdk = sdk + self.get = response_helpers.to_streamed_response_wrapper( + sdk.get, "extra_headers" + ) + + +class AsyncFiles(AsyncBaseSDK): + @property + def with_raw_response(self): + return AsyncFilesWithRawResponse(self) + + @property + def with_streaming_response(self): + return AsyncFilesWithStreamingResponse(self) + + async def get( + self, + environment: str, + path: str, + *, + api_version: Optional[str] = None, + page_size: Optional[int] = None, + page_token: Optional[str] = None, + recursive: Optional[bool] = None, + extra_headers: Optional[Mapping[str, str]] = None, + extra_query: Optional[Mapping[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + ) -> environments.GetEnvironmentFilesResponse: + r"""Retrieves a file or directory from an environment's snapshot. + + :param environment: + :param path: + :param api_version: Which version of the API to use. + :param page_size: Optional. Maximum number of entries to return per page (for directory listing). + :param page_token: Optional. Pagination token for directory listing. + :param recursive: Optional. If true and the path is a directory, recursively lists all files. + :param extra_headers: Additional headers to set or replace on requests. + :param extra_query: Additional query parameters to append to requests. + :param timeout: Override the default request timeout configuration for this method in seconds + """ + base_url = None + url_variables = None + retries: OptionalNullable[utils.RetryConfig] = UNSET + server_url = None + http_headers = extra_headers + timeout_ms = self._coerce_timeout_ms(timeout) + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + request = models.GetEnvironmentFilesRequest( + api_version=api_version, + environment=environment, + path=path, + page_size=page_size, + page_token=page_token, + recursive=recursive, + ) + + _speakeasy_response_mode, http_headers = response_helpers.consume_response_mode( + http_headers + ) + req = self._build_request_async( + method="GET", + path="/{api_version}/environments/{environment}/files/{path}", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=True, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + extra_query_params=extra_query, + _globals=models.GetEnvironmentFilesGlobals( + api_version=self.sdk_configuration.globals.api_version, + ), + security=self.sdk_configuration.security, + allow_empty_value=None, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "attempt-count-backoff", + utils.BackoffStrategy(500, 8000, 2, 30000), + True, + max_retries=4, + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["408", "409", "429", "5XX"]) + + async def _speakeasy_parse_response(http_res): + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response( + environments.GetEnvironmentFilesResponse, http_res, validate=False + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.GenAiDefaultError( + "API error occurred", http_res, http_res_text + ) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.GenAiDefaultError( + "API error occurred", http_res, http_res_text + ) + + raise errors.GenAiDefaultError("Unexpected response received", http_res) + + _speakeasy_hook_ctx = HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="GetEnvironmentFiles", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, types.Security + ), + tags=None, + extensions=None, + response=ResponseContext(mode=_speakeasy_response_mode, execution="async"), + ) + http_res = await self.do_request_async( + hook_ctx=_speakeasy_hook_ctx, + request=req, + is_error_status_code=lambda c: utils.match_status_codes(["4XX", "5XX"], c), + stream=_speakeasy_response_mode == "streaming", + retry_config=retry_config, + ) + if _speakeasy_response_mode != "parsed": + if utils.match_status_codes(["4XX", "5XX"], http_res.status_code): + await http_res.aread() + try: + await _speakeasy_parse_response(http_res) + except Exception as parse_exc_: + await response_helpers.raise_parse_error_async( + self.sdk_configuration.__dict__.get("_async_hooks"), + self.sdk_configuration.__dict__.get("_hooks"), + AfterParseErrorContext(_speakeasy_hook_ctx), + http_res, + parse_exc_, + ) + _speakeasy_response_cls = ( + response_helpers.AsyncStreamedAPIResponse + if _speakeasy_response_mode == "streaming" + else response_helpers.AsyncAPIResponse + ) + return cast( + Any, + _speakeasy_response_cls( + raw=http_res, + parser=_speakeasy_parse_response, + mode="buffered", + client_ref=self, + hook_ctx=AfterParseErrorContext(_speakeasy_hook_ctx), + hooks=self.sdk_configuration.__dict__.get("_hooks"), + async_hooks=self.sdk_configuration.__dict__.get("_async_hooks"), + ), + ) + try: + return await _speakeasy_parse_response(http_res) + except Exception as parse_exc_: + await response_helpers.raise_parse_error_async( + self.sdk_configuration.__dict__.get("_async_hooks"), + self.sdk_configuration.__dict__.get("_hooks"), + AfterParseErrorContext(_speakeasy_hook_ctx), + http_res, + parse_exc_, + ) + + +class AsyncFilesWithRawResponse: + def __init__(self, sdk: AsyncFiles) -> None: + self._sdk = sdk + self.get = response_helpers.async_to_raw_response_wrapper( + sdk.get, "extra_headers" + ) + + +class AsyncFilesWithStreamingResponse: + def __init__(self, sdk: AsyncFiles) -> None: + self._sdk = sdk + self.get = response_helpers.async_to_streamed_response_wrapper( + sdk.get, "extra_headers" + ) diff --git a/google/genai/_gaos/google_genai.py b/google/genai/_gaos/google_genai.py index dc9b3e44c..295f4b9a9 100644 --- a/google/genai/_gaos/google_genai.py +++ b/google/genai/_gaos/google_genai.py @@ -45,6 +45,7 @@ wrap_stream_errors, ) from .sdk import AsyncGenAI, GenAI +from .types import environments from .types import interactions from .types.security import Security from .utils import BackoffStrategy, RetryConfig, eventstreaming @@ -761,10 +762,90 @@ async def list_executions(self, *args: Any, **kwargs: Any) -> Any: return await async_wrap_sdk_call(super().list_executions, *args, **kwargs) +class GeminiNextGenEnvironmentFiles: + """Environment files resource backed by the NextGen client.""" + + def __init__(self, parent: GeminiNextGenEnvironments): + self._parent = parent + + def get(self, *args: Any, **kwargs: Any) -> Any: + files_sdk = getattr(self._parent, '_sdk_files', None) + if files_sdk is not None and hasattr(files_sdk, 'get'): + return wrap_sdk_call(files_sdk.get, *args, **kwargs) + raise AttributeError( + 'environments.files.get is not available on this client.' + ) + + def list(self, *args: Any, **kwargs: Any) -> Any: + """Lists directory contents or files inside an environment workspace.""" + return self.get(*args, **kwargs) + + def download( + self, + *, + environment: str, + path: str, + http_options: Optional[Any] = None, + ) -> bytes: + """Downloads binary file content from an environment workspace.""" + env_name = ( + environment + if environment.startswith('environments/') + else f'environments/{environment}' + ) + clean_path = path.lstrip('/') + download_path = f'{env_name}/files/{clean_path}?alt=media' + return self._parent._api_client.download_file( + download_path, + http_options=http_options, + ) + + +class AsyncGeminiNextGenEnvironmentFiles: + """Async environment files resource backed by the NextGen client.""" + + def __init__(self, parent: AsyncGeminiNextGenEnvironments): + self._parent = parent + + async def get(self, *args: Any, **kwargs: Any) -> Any: + files_sdk = getattr(self._parent, '_sdk_files', None) + if files_sdk is not None and hasattr(files_sdk, 'get'): + return await async_wrap_sdk_call(files_sdk.get, *args, **kwargs) + raise AttributeError( + 'environments.files.get is not available on this client.' + ) + + async def list(self, *args: Any, **kwargs: Any) -> Any: + """Lists directory contents or files inside an environment workspace.""" + return await self.get(*args, **kwargs) + + async def download( + self, + *, + environment: str, + path: str, + http_options: Optional[Any] = None, + ) -> bytes: + """Downloads binary file content from an environment workspace.""" + env_name = ( + environment + if environment.startswith('environments/') + else f'environments/{environment}' + ) + clean_path = path.lstrip('/') + download_path = f'{env_name}/files/{clean_path}?alt=media' + return await self._parent._api_client.async_download_file( + download_path, + http_options=http_options, + ) + + class GeminiNextGenEnvironments(GeneratedEnvironments): """Public environments resource backed by the NextGen client.""" def __init__(self, api_client: Any): + self._api_client = api_client + self._files_wrapper = GeminiNextGenEnvironmentFiles(self) sdk = build_google_genai_client(api_client) super().__init__(sdk.sdk_configuration, parent_ref=sdk) @@ -777,6 +858,14 @@ def with_raw_response(self): def with_streaming_response(self): return _RawResponseAccessorProxy(super().with_streaming_response) + @property + def files(self) -> GeminiNextGenEnvironmentFiles: + return getattr(self, '_files_wrapper', None) + + @files.setter + def files(self, value: Any) -> None: + self._sdk_files = value + def create_environment(self, *args: Any, **kwargs: Any) -> Any: return wrap_sdk_call(super().create_environment, *args, **kwargs) @@ -801,16 +890,13 @@ def delete_environment(self, *args: Any, **kwargs: Any) -> Any: def delete(self, *args: Any, **kwargs: Any) -> Any: return self.delete_environment(*args, **kwargs) - def get_environment_files(self, *args: Any, **kwargs: Any) -> Any: - return wrap_sdk_call(super().get_environment_files, *args, **kwargs) - - # NOTE: update_environment, patch_environment are handled by fallback if they exist, but we assume they aren't generated based on our openapi.json. - class AsyncGeminiNextGenEnvironments(GeneratedAsyncEnvironments): """Async public environments resource backed by the NextGen client.""" def __init__(self, api_client: Any): + self._api_client = api_client + self._files_wrapper = AsyncGeminiNextGenEnvironmentFiles(self) sdk = build_google_genai_async_client(api_client) super().__init__(sdk.sdk_configuration, parent_ref=sdk) @@ -823,6 +909,14 @@ def with_raw_response(self): def with_streaming_response(self): return _AsyncRawResponseAccessorProxy(super().with_streaming_response) + @property + def files(self) -> AsyncGeminiNextGenEnvironmentFiles: + return getattr(self, '_files_wrapper', None) + + @files.setter + def files(self, value: Any) -> None: + self._sdk_files = value + async def create_environment(self, *args: Any, **kwargs: Any) -> Any: return await async_wrap_sdk_call(super().create_environment, *args, **kwargs) @@ -847,9 +941,6 @@ async def delete_environment(self, *args: Any, **kwargs: Any) -> Any: async def delete(self, *args: Any, **kwargs: Any) -> Any: return await self.delete_environment(*args, **kwargs) - async def get_environment_files(self, *args: Any, **kwargs: Any) -> Any: - return await async_wrap_sdk_call(super().get_environment_files, *args, **kwargs) - def _add_output_properties_if_interaction(value: Any) -> Any: normalized = _normalize_interaction_shape(value) diff --git a/google/genai/_gaos/models/__init__.py b/google/genai/_gaos/models/__init__.py index 024cabd61..428404648 100644 --- a/google/genai/_gaos/models/__init__.py +++ b/google/genai/_gaos/models/__init__.py @@ -103,6 +103,12 @@ GetEnvironmentRequest, GetEnvironmentRequestParam, ) + from .getenvironmentfiles import ( + GetEnvironmentFilesGlobals, + GetEnvironmentFilesGlobalsTypedDict, + GetEnvironmentFilesRequest, + GetEnvironmentFilesRequestParam, + ) from .getinteractionbyid import ( GetInteractionByIDGlobals, GetInteractionByIDGlobalsTypedDict, @@ -238,6 +244,10 @@ "GetAgentGlobalsTypedDict", "GetAgentRequest", "GetAgentRequestParam", + "GetEnvironmentFilesGlobals", + "GetEnvironmentFilesGlobalsTypedDict", + "GetEnvironmentFilesRequest", + "GetEnvironmentFilesRequestParam", "GetEnvironmentGlobals", "GetEnvironmentGlobalsTypedDict", "GetEnvironmentRequest", @@ -355,6 +365,10 @@ "GetEnvironmentGlobalsTypedDict": ".getenvironment", "GetEnvironmentRequest": ".getenvironment", "GetEnvironmentRequestParam": ".getenvironment", + "GetEnvironmentFilesGlobals": ".getenvironmentfiles", + "GetEnvironmentFilesGlobalsTypedDict": ".getenvironmentfiles", + "GetEnvironmentFilesRequest": ".getenvironmentfiles", + "GetEnvironmentFilesRequestParam": ".getenvironmentfiles", "GetInteractionByIDGlobals": ".getinteractionbyid", "GetInteractionByIDGlobalsTypedDict": ".getinteractionbyid", "GetInteractionByIDRequest": ".getinteractionbyid", diff --git a/google/genai/_gaos/models/getenvironmentfiles.py b/google/genai/_gaos/models/getenvironmentfiles.py new file mode 100644 index 000000000..4ff3c479c --- /dev/null +++ b/google/genai/_gaos/models/getenvironmentfiles.py @@ -0,0 +1,116 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pyformat: disable + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from ..types import BaseModel, UNSET_SENTINEL +from ..utils import FieldMetadata, PathParamMetadata, QueryParamMetadata +from pydantic import model_serializer +from typing import Optional +from typing_extensions import Annotated, NotRequired, TypedDict + + +class GetEnvironmentFilesGlobalsTypedDict(TypedDict): + api_version: NotRequired[str] + r"""Which version of the API to use.""" + + +class GetEnvironmentFilesGlobals(BaseModel): + api_version: Annotated[ + Optional[str], + FieldMetadata(path=PathParamMetadata(style="simple", explode=False)), + ] = None + r"""Which version of the API to use.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["api_version"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m + + +class GetEnvironmentFilesRequestParam(TypedDict): + environment: str + path: str + api_version: NotRequired[str] + r"""Which version of the API to use.""" + page_size: NotRequired[int] + r"""Optional. Maximum number of entries to return per page (for directory listing).""" + page_token: NotRequired[str] + r"""Optional. Pagination token for directory listing.""" + recursive: NotRequired[bool] + r"""Optional. If true and the path is a directory, recursively lists all files.""" + + +class GetEnvironmentFilesRequest(BaseModel): + environment: Annotated[ + str, FieldMetadata(path=PathParamMetadata(style="simple", explode=False)) + ] + + path: Annotated[ + str, FieldMetadata(path=PathParamMetadata(style="simple", explode=False)) + ] + + api_version: Annotated[ + Optional[str], + FieldMetadata(path=PathParamMetadata(style="simple", explode=False)), + ] = None + r"""Which version of the API to use.""" + + page_size: Annotated[ + Optional[int], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""Optional. Maximum number of entries to return per page (for directory listing).""" + + page_token: Annotated[ + Optional[str], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""Optional. Pagination token for directory listing.""" + + recursive: Annotated[ + Optional[bool], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""Optional. If true and the path is a directory, recursively lists all files.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["api_version", "page_size", "page_token", "recursive"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/google/genai/_gaos/resources/environments/__init__.py b/google/genai/_gaos/resources/environments/__init__.py index dd96730ea..5e59b803e 100644 --- a/google/genai/_gaos/resources/environments/__init__.py +++ b/google/genai/_gaos/resources/environments/__init__.py @@ -18,6 +18,10 @@ from ...types.environments.createenvironmentrequest import CreateEnvironmentRequest from ...types.environments.environment import Environment +from ...types.environments.environmentfile import EnvironmentFile +from ...types.environments.getenvironmentfilesresponse import ( + GetEnvironmentFilesResponse, +) from ...types.environments.listenvironmentsresponse import ( ListEnvironmentsResponse as EnvironmentListResponse, ) @@ -29,7 +33,9 @@ "CreateEnvironmentRequest", "Environment", "EnvironmentDeleteResponse", + "EnvironmentFile", "EnvironmentListResponse", + "GetEnvironmentFilesResponse", "createenvironmentrequest", "environment", ] diff --git a/google/genai/_gaos/types/environments/__init__.py b/google/genai/_gaos/types/environments/__init__.py index 827c0a619..0ded33030 100644 --- a/google/genai/_gaos/types/environments/__init__.py +++ b/google/genai/_gaos/types/environments/__init__.py @@ -36,6 +36,11 @@ EnvironmentTypedDict, Status, ) + from .environmentfile import EnvironmentFile, EnvironmentFileTypedDict, Type + from .getenvironmentfilesresponse import ( + GetEnvironmentFilesResponse, + GetEnvironmentFilesResponseTypedDict, + ) from .listenvironmentsresponse import ( ListEnvironmentsResponse, ListEnvironmentsResponseTypedDict, @@ -48,13 +53,18 @@ "CreateEnvironmentRequestNetworkUnionParam", "CreateEnvironmentRequestParam", "Environment", + "EnvironmentFile", + "EnvironmentFileTypedDict", "EnvironmentNetworkEnum", "EnvironmentNetworkUnion", "EnvironmentNetworkUnionTypedDict", "EnvironmentTypedDict", + "GetEnvironmentFilesResponse", + "GetEnvironmentFilesResponseTypedDict", "ListEnvironmentsResponse", "ListEnvironmentsResponseTypedDict", "Status", + "Type", ] _dynamic_imports: dict[str, str] = { @@ -69,6 +79,11 @@ "EnvironmentNetworkUnionTypedDict": ".environment", "EnvironmentTypedDict": ".environment", "Status": ".environment", + "EnvironmentFile": ".environmentfile", + "EnvironmentFileTypedDict": ".environmentfile", + "Type": ".environmentfile", + "GetEnvironmentFilesResponse": ".getenvironmentfilesresponse", + "GetEnvironmentFilesResponseTypedDict": ".getenvironmentfilesresponse", "ListEnvironmentsResponse": ".listenvironmentsresponse", "ListEnvironmentsResponseTypedDict": ".listenvironmentsresponse", } diff --git a/google/genai/_gaos/types/environments/environmentfile.py b/google/genai/_gaos/types/environments/environmentfile.py new file mode 100644 index 000000000..080b7ea3d --- /dev/null +++ b/google/genai/_gaos/types/environments/environmentfile.py @@ -0,0 +1,117 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pyformat: disable + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .. import BaseModel, UNSET_SENTINEL, UnrecognizedStr +from ...utils import serialize_int, validate_int +from datetime import datetime +from pydantic import model_serializer +from pydantic.functional_serializers import PlainSerializer +from pydantic.functional_validators import BeforeValidator +from typing import Literal, Optional, Union +from typing_extensions import Annotated, NotRequired, TypedDict + + +Type = Union[ + Literal[ + "file", + "directory", + ], + UnrecognizedStr, +] +r"""Output only. The type of the entry.""" + + +class EnvironmentFileTypedDict(TypedDict): + r"""Metadata for a file or directory within an environment.""" + + created: NotRequired[datetime] + r"""Output only. The creation time of the file/directory.""" + mime_type: NotRequired[str] + r"""Output only. The MIME type of the file (e.g., \"text/python\", \"image/png\"). + Empty for directories. + NOLINT + """ + modified: NotRequired[datetime] + r"""Output only. The modification time of the file/directory.""" + name: NotRequired[str] + r"""Output only. The name of the file or directory (e.g., \"main.py\" or \"src\").""" + path: NotRequired[str] + r"""Output only. The full relative path within the environment + (e.g., \"workspace/src/main.py\"). + """ + size_bytes: NotRequired[int] + r"""Output only. The size of the file/directory in bytes. + NOLINT + """ + type: NotRequired[Type] + r"""Output only. The type of the entry.""" + + +class EnvironmentFile(BaseModel): + r"""Metadata for a file or directory within an environment.""" + + created: Optional[datetime] = None + r"""Output only. The creation time of the file/directory.""" + + mime_type: Optional[str] = None + r"""Output only. The MIME type of the file (e.g., \"text/python\", \"image/png\"). + Empty for directories. + NOLINT + """ + + modified: Optional[datetime] = None + r"""Output only. The modification time of the file/directory.""" + + name: Optional[str] = None + r"""Output only. The name of the file or directory (e.g., \"main.py\" or \"src\").""" + + path: Optional[str] = None + r"""Output only. The full relative path within the environment + (e.g., \"workspace/src/main.py\"). + """ + + size_bytes: Annotated[ + Optional[int], + BeforeValidator(validate_int), + PlainSerializer(serialize_int(True)), + ] = None + r"""Output only. The size of the file/directory in bytes. + NOLINT + """ + + type: Optional[Type] = None + r"""Output only. The type of the entry.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set( + ["created", "mime_type", "modified", "name", "path", "size_bytes", "type"] + ) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/google/genai/_gaos/types/environments/getenvironmentfilesresponse.py b/google/genai/_gaos/types/environments/getenvironmentfilesresponse.py new file mode 100644 index 000000000..c645d5c9b --- /dev/null +++ b/google/genai/_gaos/types/environments/getenvironmentfilesresponse.py @@ -0,0 +1,71 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# pyformat: disable + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from .. import BaseModel, UNSET_SENTINEL +from .environmentfile import EnvironmentFile, EnvironmentFileTypedDict +from pydantic import model_serializer +from typing import List, Optional +from typing_extensions import NotRequired, TypedDict + + +class GetEnvironmentFilesResponseTypedDict(TypedDict): + r"""Response for `GetEnvironmentFiles`.""" + + files: NotRequired[List[EnvironmentFileTypedDict]] + r"""If the requested path is a directory, this contains its contents. + If the requested path is a file, this contains a single entry with the + file's metadata. + If alt=media was specified, this is empty (content is served via `blob`). + """ + next_page_token: NotRequired[str] + r"""Pagination token for directory listing. + NOLINT + """ + + +class GetEnvironmentFilesResponse(BaseModel): + r"""Response for `GetEnvironmentFiles`.""" + + files: Optional[List[EnvironmentFile]] = None + r"""If the requested path is a directory, this contains its contents. + If the requested path is a file, this contains a single entry with the + file's metadata. + If alt=media was specified, this is empty (content is served via `blob`). + """ + + next_page_token: Optional[str] = None + r"""Pagination token for directory listing. + NOLINT + """ + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["files", "next_page_token"]) + serialized = handler(self) + m = {} + + for n, f in type(self).model_fields.items(): + k = f.alias or n + val = serialized.get(k, serialized.get(n)) + + if val != UNSET_SENTINEL: + if val is not None or k not in optional_fields: + m[k] = val + + return m diff --git a/google/genai/tests/gaos/test_environments_lifecycle.py b/google/genai/tests/gaos/test_environments_lifecycle.py index bc1fdb6b6..13dd58cf5 100644 --- a/google/genai/tests/gaos/test_environments_lifecycle.py +++ b/google/genai/tests/gaos/test_environments_lifecycle.py @@ -22,6 +22,15 @@ import pytest from ... import Client +from ..._gaos.google_genai import ( + AsyncGeminiNextGenEnvironmentFiles, + GeminiNextGenEnvironmentFiles, +) +from ..._gaos.models.getenvironmentfiles import GetEnvironmentFilesRequest +from ..._gaos.types.environments.environmentfile import EnvironmentFile +from ..._gaos.types.environments.getenvironmentfilesresponse import ( + GetEnvironmentFilesResponse, +) ENVIRONMENT_BODY = { "id": "env_abc_1234", @@ -37,6 +46,21 @@ ], } +ENVIRONMENT_FILES_PAYLOAD = { + "files": [ + { + "name": "main.py", + "path": "workspace/src/main.py", + "type": "file", + "size_bytes": "128", + "mime_type": "text/x-python", + "created": "2026-07-22T15:18:38Z", + "modified": "2026-07-22T15:18:38Z", + } + ], + "next_page_token": "token_next_123", +} + class _RecordingHandler(BaseHTTPRequestHandler): captured: list[str] = [] @@ -116,3 +140,239 @@ def test_python_environments_lifecycle_routes_through_google_genai_client( server.shutdown() thread.join() server.server_close() + + +class _ScottyDownloadHandler(BaseHTTPRequestHandler): + captured: list[str] = [] + + def do_GET(self) -> None: + self.captured.append(f"GET {self.path}") + if "?alt=media" in self.path or "&alt=media" in self.path: + payload = b"print('downloaded content')\n" + self.send_response(200) + self.send_header("content-type", "application/octet-stream") + self.send_header("content-length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + return + elif "/files" in self.path: + payload = json.dumps(ENVIRONMENT_FILES_PAYLOAD).encode() + self.send_response(200) + self.send_header("content-type", "application/json") + self.send_header("content-length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + return + + self.send_response(404) + self.end_headers() + + def log_message(self, *args) -> None: + pass + + +def test_python_environments_files_get_and_download(monkeypatch): + monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + captured: list[str] = [] + handler = type("Handler", (_ScottyDownloadHandler,), { + "captured": captured, + }) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + client = Client( + api_key="test-api-key", + http_options={ + "api_version": "v1beta", + "base_url": f"http://127.0.0.1:{server.server_port}", + "headers": {"X-Goog-Api-Client": "test"}, + }, + ) + + # Test sync files.get basic + files_res = client.environments.files.get( + environment="env_123", + path="src/main.py", + ) + assert len(files_res.files) == 1 + assert files_res.files[0].name == "main.py" + assert files_res.files[0].path == "workspace/src/main.py" + assert files_res.files[0].type == "file" + assert files_res.files[0].size_bytes == 128 + assert files_res.next_page_token == "token_next_123" + + # Test sync files.get with pagination and recursive options + files_res_paginated = client.environments.files.get( + environment="env_123", + path="src", + page_size=10, + page_token="token_start", + recursive=True, + ) + assert len(files_res_paginated.files) == 1 + + # Test sync files.list + files_res_list = client.environments.files.list( + environment="env_123", + path="src", + page_size=10, + page_token="token_start", + recursive=True, + ) + assert len(files_res_list.files) == 1 + + # Test sync files.download + downloaded = client.environments.files.download( + environment="env_123", + path="src/main.py", + ) + assert downloaded == b"print('downloaded content')\n" + + # Test sync files.download with full resource name and leading slash + downloaded_full = client.environments.files.download( + environment="environments/env_123", + path="/src/main.py", + ) + assert downloaded_full == b"print('downloaded content')\n" + + assert any("page_size=10" in call for call in captured) + assert any("page_token=token_start" in call for call in captured) + assert any("recursive=true" in call for call in captured) + + finally: + server.shutdown() + thread.join() + server.server_close() + + +@pytest.mark.asyncio +async def test_python_environments_async_files_get_and_download(monkeypatch): + monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + captured: list[str] = [] + handler = type("Handler", (_ScottyDownloadHandler,), { + "captured": captured, + }) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + client = Client( + api_key="test-api-key", + http_options={ + "api_version": "v1beta", + "base_url": f"http://127.0.0.1:{server.server_port}", + }, + ) + + # Test async files.get basic + files_res = await client.aio.environments.files.get( + environment="env_123", + path="src/main.py", + ) + assert len(files_res.files) == 1 + assert files_res.files[0].name == "main.py" + assert files_res.files[0].path == "workspace/src/main.py" + assert files_res.files[0].type == "file" + assert files_res.files[0].size_bytes == 128 + + # Test async files.get with pagination and recursive options + files_res_paginated = await client.aio.environments.files.get( + environment="env_123", + path="src", + page_size=10, + page_token="token_start", + recursive=True, + ) + assert len(files_res_paginated.files) == 1 + + # Test async files.list + files_res_list = await client.aio.environments.files.list( + environment="env_123", + path="src", + page_size=10, + page_token="token_start", + recursive=True, + ) + assert len(files_res_list.files) == 1 + + # Test async files.download + downloaded = await client.aio.environments.files.download( + environment="env_123", + path="src/main.py", + ) + assert downloaded == b"print('downloaded content')\n" + + # Test async files.download with full resource name and leading slash + downloaded_full = await client.aio.environments.files.download( + environment="environments/env_123", + path="/src/main.py", + ) + assert downloaded_full == b"print('downloaded content')\n" + + finally: + server.shutdown() + thread.join() + server.server_close() + + +def test_python_environments_files_wrapper_error_handling(): + wrapper = GeminiNextGenEnvironmentFiles(parent=object()) + with pytest.raises( + AttributeError, + match="environments.files.get is not available on this client.", + ): + wrapper.get(environment="env_123", path="src/main.py") + + async_wrapper = AsyncGeminiNextGenEnvironmentFiles(parent=object()) + import asyncio + + with pytest.raises( + AttributeError, + match="environments.files.get is not available on this client.", + ): + asyncio.run(async_wrapper.get(environment="env_123", path="src/main.py")) + + +def test_python_environments_types_and_models(): + file_obj = EnvironmentFile( + created="2026-07-22T15:18:38Z", + mime_type="text/x-python", + modified="2026-07-22T15:18:38Z", + name="main.py", + path="workspace/src/main.py", + size_bytes=128, + type="file", + ) + assert file_obj.name == "main.py" + assert file_obj.path == "workspace/src/main.py" + assert file_obj.type == "file" + assert file_obj.size_bytes == 128 + assert file_obj.mime_type == "text/x-python" + assert file_obj.created is not None + assert file_obj.modified is not None + + response = GetEnvironmentFilesResponse( + files=[file_obj], + next_page_token="next_tok", + ) + assert len(response.files) == 1 + assert response.next_page_token == "next_tok" + + req = GetEnvironmentFilesRequest( + environment="env_123", + path="src/main.py", + page_size=20, + page_token="tok", + recursive=True, + api_version="v1beta", + ) + assert req.environment == "env_123" + assert req.path == "src/main.py" + assert req.page_size == 20 + assert req.page_token == "tok" + assert req.recursive is True + assert req.api_version == "v1beta" + + +