diff --git a/google/genai/_gaos/_internal.py b/google/genai/_gaos/_internal.py new file mode 100644 index 000000000..4d777a723 --- /dev/null +++ b/google/genai/_gaos/_internal.py @@ -0,0 +1,437 @@ +# 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 +# pylint: skip-file + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from .basesdk import AsyncBaseSDK, BaseSDK +from . import errors, models, types, utils +from ._hooks import AfterParseErrorContext, HookContext, ResponseContext +from .types import OptionalNullable, UNSET +from .utils import get_security_from_env, response_helpers +import httpx +from typing import Any, Mapping, Optional, Union, cast + + +class Internal(BaseSDK): + @property + def with_raw_response(self): + return InternalWithRawResponse(self) + + @property + def with_streaming_response(self): + return InternalWithStreamingResponse(self) + + def start_upload( + self, + environment: str, + path: str, + *, + x_goog_upload_header_content_length: int, + x_goog_upload_header_content_type: str, + api_version: Optional[str] = None, + extract: Optional[bool] = None, + overwrite: Optional[bool] = None, + extra_headers: Optional[Mapping[str, str]] = None, + extra_query: Optional[Mapping[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + ) -> models.StartEnvironmentFileUploadResponse: + r"""Start an environment file upload + + Starts a resumable upload session for a file in an environment workspace. + Upload the file bytes to the URL returned in the `X-Goog-Upload-URL` + response header, using the resumable upload protocol. + + + :param environment: The ID of the environment that owns the destination file. + :param path: The relative destination path inside the environment workspace. + :param x_goog_upload_header_content_length: Total number of file bytes that will be uploaded to the session URL. + :param x_goog_upload_header_content_type: MIME type of the file that will be uploaded to the session URL. + :param api_version: Which version of the API to use. + :param extract: Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`. + :param overwrite: Optional. Whether to overwrite the destination file if it already exists. + :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.StartEnvironmentFileUploadRequest( + api_version=api_version, + environment=environment, + path=path, + extract=extract, + overwrite=overwrite, + x_goog_upload_header_content_length=x_goog_upload_header_content_length, + x_goog_upload_header_content_type=x_goog_upload_header_content_type, + ) + + _speakeasy_response_mode, http_headers = response_helpers.consume_response_mode( + http_headers + ) + req = self._build_request( + method="PUT", + path="/upload/{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="*/*", + http_headers=http_headers, + extra_query_params=extra_query, + _globals=models.StartEnvironmentFileUploadGlobals( + 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", "*"): + return models.StartEnvironmentFileUploadResponse( + headers=utils.get_response_headers(http_res.headers) + ) + 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="StartEnvironmentFileUpload", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, types.Security + ), + tags=None, + extensions={ + "x-codeSamples": [ + { + "label": "start_upload", + "lang": "sh", + "source": "curl -i -X PUT \\\n 'https://generativelanguage.googleapis.com/upload/v1beta/environments/env_abc123/files/main.py?overwrite=true' \\\n -H \"x-goog-api-key: $GEMINI_API_KEY\" \\\n -H 'X-Goog-Upload-Protocol: resumable' \\\n -H 'X-Goog-Upload-Command: start' \\\n -H \"X-Goog-Upload-Header-Content-Length: $(wc -c < main.py)\" \\\n -H 'X-Goog-Upload-Header-Content-Type: text/x-python'\n", + } + ] + }, + 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 InternalWithRawResponse: + def __init__(self, sdk: Internal) -> None: + self._sdk = sdk + self.start_upload = response_helpers.to_raw_response_wrapper( + sdk.start_upload, "extra_headers" + ) + + +class InternalWithStreamingResponse: + def __init__(self, sdk: Internal) -> None: + self._sdk = sdk + self.start_upload = response_helpers.to_streamed_response_wrapper( + sdk.start_upload, "extra_headers" + ) + + +class AsyncInternal(AsyncBaseSDK): + @property + def with_raw_response(self): + return AsyncInternalWithRawResponse(self) + + @property + def with_streaming_response(self): + return AsyncInternalWithStreamingResponse(self) + + async def start_upload( + self, + environment: str, + path: str, + *, + x_goog_upload_header_content_length: int, + x_goog_upload_header_content_type: str, + api_version: Optional[str] = None, + extract: Optional[bool] = None, + overwrite: Optional[bool] = None, + extra_headers: Optional[Mapping[str, str]] = None, + extra_query: Optional[Mapping[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + ) -> models.StartEnvironmentFileUploadResponse: + r"""Start an environment file upload + + Starts a resumable upload session for a file in an environment workspace. + Upload the file bytes to the URL returned in the `X-Goog-Upload-URL` + response header, using the resumable upload protocol. + + + :param environment: The ID of the environment that owns the destination file. + :param path: The relative destination path inside the environment workspace. + :param x_goog_upload_header_content_length: Total number of file bytes that will be uploaded to the session URL. + :param x_goog_upload_header_content_type: MIME type of the file that will be uploaded to the session URL. + :param api_version: Which version of the API to use. + :param extract: Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`. + :param overwrite: Optional. Whether to overwrite the destination file if it already exists. + :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.StartEnvironmentFileUploadRequest( + api_version=api_version, + environment=environment, + path=path, + extract=extract, + overwrite=overwrite, + x_goog_upload_header_content_length=x_goog_upload_header_content_length, + x_goog_upload_header_content_type=x_goog_upload_header_content_type, + ) + + _speakeasy_response_mode, http_headers = response_helpers.consume_response_mode( + http_headers + ) + req = self._build_request_async( + method="PUT", + path="/upload/{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="*/*", + http_headers=http_headers, + extra_query_params=extra_query, + _globals=models.StartEnvironmentFileUploadGlobals( + 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", "*"): + return models.StartEnvironmentFileUploadResponse( + headers=utils.get_response_headers(http_res.headers) + ) + 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="StartEnvironmentFileUpload", + oauth2_scopes=None, + security_source=get_security_from_env( + self.sdk_configuration.security, types.Security + ), + tags=None, + extensions={ + "x-codeSamples": [ + { + "label": "start_upload", + "lang": "sh", + "source": "curl -i -X PUT \\\n 'https://generativelanguage.googleapis.com/upload/v1beta/environments/env_abc123/files/main.py?overwrite=true' \\\n -H \"x-goog-api-key: $GEMINI_API_KEY\" \\\n -H 'X-Goog-Upload-Protocol: resumable' \\\n -H 'X-Goog-Upload-Command: start' \\\n -H \"X-Goog-Upload-Header-Content-Length: $(wc -c < main.py)\" \\\n -H 'X-Goog-Upload-Header-Content-Type: text/x-python'\n", + } + ] + }, + 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 AsyncInternalWithRawResponse: + def __init__(self, sdk: AsyncInternal) -> None: + self._sdk = sdk + self.start_upload = response_helpers.async_to_raw_response_wrapper( + sdk.start_upload, "extra_headers" + ) + + +class AsyncInternalWithStreamingResponse: + def __init__(self, sdk: AsyncInternal) -> None: + self._sdk = sdk + self.start_upload = response_helpers.async_to_streamed_response_wrapper( + sdk.start_upload, "extra_headers" + ) diff --git a/google/genai/_gaos/environments.py b/google/genai/_gaos/environments.py index db2e4a41c..1ed0f8983 100644 --- a/google/genai/_gaos/environments.py +++ b/google/genai/_gaos/environments.py @@ -21,6 +21,7 @@ from .sdkconfiguration import SDKConfiguration from . import errors, models, types, utils from ._hooks import AfterParseErrorContext, HookContext, ResponseContext +from ._internal import AsyncInternal, Internal from .files import AsyncFiles, Files from .types import OptionalNullable, UNSET, environments, interactions from .types.environments import ( @@ -42,6 +43,7 @@ def with_raw_response(self): def with_streaming_response(self): return EnvironmentsWithStreamingResponse(self) + internal: Internal files: Files def __init__( @@ -52,6 +54,7 @@ def __init__( self._init_sdks() def _init_sdks(self): + self.internal = Internal(self.sdk_configuration, parent_ref=self.parent_ref) self.files = Files(self.sdk_configuration, parent_ref=self.parent_ref) def list_environments( @@ -800,6 +803,10 @@ def __init__(self, sdk: Environments) -> None: sdk.get_environment, "extra_headers" ) + @property + def internal(self): + return self._sdk.internal.with_raw_response + @property def files(self): return self._sdk.files.with_raw_response @@ -821,6 +828,10 @@ def __init__(self, sdk: Environments) -> None: sdk.get_environment, "extra_headers" ) + @property + def internal(self): + return self._sdk.internal.with_streaming_response + @property def files(self): return self._sdk.files.with_streaming_response @@ -835,6 +846,7 @@ def with_raw_response(self): def with_streaming_response(self): return AsyncEnvironmentsWithStreamingResponse(self) + internal: AsyncInternal files: AsyncFiles def __init__( @@ -845,6 +857,9 @@ def __init__( self._init_sdks() def _init_sdks(self): + self.internal = AsyncInternal( + self.sdk_configuration, parent_ref=self.parent_ref + ) self.files = AsyncFiles(self.sdk_configuration, parent_ref=self.parent_ref) async def list_environments( @@ -1605,6 +1620,10 @@ def __init__(self, sdk: AsyncEnvironments) -> None: sdk.get_environment, "extra_headers" ) + @property + def internal(self): + return self._sdk.internal.with_raw_response + @property def files(self): return self._sdk.files.with_raw_response @@ -1626,6 +1645,10 @@ def __init__(self, sdk: AsyncEnvironments) -> None: sdk.get_environment, "extra_headers" ) + @property + def internal(self): + return self._sdk.internal.with_streaming_response + @property def files(self): return self._sdk.files.with_streaming_response diff --git a/google/genai/_gaos/google_genai.py b/google/genai/_gaos/google_genai.py index dde0284e1..ef1117e70 100644 --- a/google/genai/_gaos/google_genai.py +++ b/google/genai/_gaos/google_genai.py @@ -26,6 +26,11 @@ from typing import TYPE_CHECKING, Any, Mapping, Optional, TypeVar, Union, cast +import io +import json +import mimetypes +import os + import httpx from ._hooks.google_genai_auth import ( @@ -61,6 +66,8 @@ from .credentials import Credentials as GeneratedCredentials from .files import AsyncFiles as GeneratedAsyncFiles from .files import Files as GeneratedFiles +from ._internal import AsyncInternal as GeneratedAsyncInternal +from ._internal import Internal as GeneratedInternal GOOGLE_GENAI_API_REVISION = _GOOGLE_GENAI_API_REVISION @@ -838,6 +845,7 @@ async def list_executions(self, *args: Any, **kwargs: Any) -> Any: return await async_wrap_sdk_call(super().list_executions, *args, **kwargs) + class GeminiNextGenEnvironmentFiles(GeneratedFiles): """Environment files resource backed by the NextGen client.""" @@ -849,6 +857,7 @@ def __init__( ): super().__init__(sdk_config, parent_ref=parent_ref) self._api_client = api_client + self._internal = GeneratedInternal(sdk_config, parent_ref=parent_ref) if not TYPE_CHECKING: @property @@ -866,17 +875,21 @@ def list(self, *args: Any, **kwargs: Any) -> Any: def download( self, *, - environment: str, path: str, + environment: Optional[str] = None, + environment_id: Optional[str] = None, http_options: Optional[Any] = None, ) -> bytes: """Downloads binary file content from an environment workspace.""" if not self._api_client: raise AttributeError('api_client is required to download files.') + target_env = environment or environment_id + if not target_env: + raise ValueError('environment or environment_id is required.') env_name = ( - environment - if environment.startswith('environments/') - else f'environments/{environment}' + target_env + if target_env.startswith('environments/') + else f'environments/{target_env}' ) clean_path = path.lstrip('/') download_path = f'{env_name}/files/{clean_path}?alt=media' @@ -885,6 +898,111 @@ def download( http_options=http_options, ) + def upload( + self, + *, + path: str, + file: Union[str, os.PathLike[str], io.IOBase, bytes], + environment: Optional[str] = None, + environment_id: Optional[str] = None, + mime_type: Optional[str] = None, + overwrite: Optional[bool] = None, + extract: Optional[bool] = None, + http_options: Optional[Any] = None, + ) -> Union[environments.GetEnvironmentFilesResponse, Any]: + """Uploads a file or extracts an archive inside an environment workspace.""" + if not self._api_client: + raise AttributeError('api_client is required to upload files.') + target_env = environment or environment_id + if not target_env: + raise ValueError('environment or environment_id is required.') + clean_env = ( + target_env.removeprefix('environments/') + if target_env.startswith('environments/') + else target_env + ) + clean_path = path.lstrip('/') + + file_obj: Union[str, io.IOBase] + if isinstance(file, (bytes, bytearray)): + file_obj = io.BytesIO(file) + size_bytes = len(file) + elif isinstance(file, io.IOBase): + file_obj = file + offset = file_obj.tell() + file_obj.seek(0, os.SEEK_END) + size_bytes = file_obj.tell() - offset + file_obj.seek(offset, os.SEEK_SET) + else: + fs_path = os.fspath(file) + if not fs_path or not os.path.isfile(fs_path): + raise FileNotFoundError(f'{file} is not a valid file path.') + size_bytes = os.path.getsize(fs_path) + file_obj = fs_path + if mime_type is None: + mime_type, _ = mimetypes.guess_type(fs_path) + + if mime_type is None: + mime_type = 'application/octet-stream' + + req_api_version = ( + ( + http_options.get('api_version') + if isinstance(http_options, dict) + else getattr(http_options, 'api_version', None) + ) + or self._api_client._http_options.api_version + or 'v1alpha' + ).lstrip('/') + + handshake_res = self._internal.start_upload( + environment=clean_env, + path=clean_path, + x_goog_upload_header_content_length=size_bytes, + x_goog_upload_header_content_type=mime_type, + api_version=req_api_version, + extract=extract, + overwrite=overwrite, + ) + + upload_urls = ( + handshake_res.headers.get('x-goog-upload-url') + or handshake_res.headers.get('X-Goog-Upload-URL') + ) + if not upload_urls: + raise KeyError( + 'Failed to upload file: Upload URL was not returned from the upload request.' + ) + upload_url = upload_urls[0] if isinstance(upload_urls, list) else upload_urls + + upload_response = self._api_client.upload_file( + file_obj, + upload_url, + size_bytes, + http_options=http_options, + ) + + body_text = ( + upload_response.response_stream[0] + if upload_response and upload_response.response_stream + else '{}' + ) + try: + res_json = json.loads(body_text) if body_text else {} + except Exception: + return body_text + + if isinstance(res_json, dict): + if 'files' in res_json and isinstance(res_json['files'], list): + return environments.GetEnvironmentFilesResponse.model_validate(res_json) + elif 'name' in res_json or 'path' in res_json: + file_obj = environments.EnvironmentFile.model_validate(res_json) + return environments.GetEnvironmentFilesResponse(files=[file_obj]) + elif 'file' in res_json and isinstance(res_json['file'], dict): + file_obj = environments.EnvironmentFile.model_validate(res_json['file']) + return environments.GetEnvironmentFilesResponse(files=[file_obj]) + return res_json + class AsyncGeminiNextGenEnvironmentFiles(GeneratedAsyncFiles): """Async environment files resource backed by the NextGen client.""" @@ -897,6 +1015,7 @@ def __init__( ): super().__init__(sdk_config, parent_ref=parent_ref) self._api_client = api_client + self._internal = GeneratedAsyncInternal(sdk_config, parent_ref=parent_ref) if not TYPE_CHECKING: @property @@ -914,17 +1033,21 @@ async def list(self, *args: Any, **kwargs: Any) -> Any: async def download( self, *, - environment: str, path: str, + environment: Optional[str] = None, + environment_id: Optional[str] = None, http_options: Optional[Any] = None, ) -> bytes: """Downloads binary file content from an environment workspace.""" if not self._api_client: raise AttributeError('api_client is required to download files.') + target_env = environment or environment_id + if not target_env: + raise ValueError('environment or environment_id is required.') env_name = ( - environment - if environment.startswith('environments/') - else f'environments/{environment}' + target_env + if target_env.startswith('environments/') + else f'environments/{target_env}' ) clean_path = path.lstrip('/') download_path = f'{env_name}/files/{clean_path}?alt=media' @@ -933,6 +1056,111 @@ async def download( http_options=http_options, ) + async def upload( + self, + *, + path: str, + file: Union[str, os.PathLike[str], io.IOBase, bytes], + environment: Optional[str] = None, + environment_id: Optional[str] = None, + mime_type: Optional[str] = None, + overwrite: Optional[bool] = None, + extract: Optional[bool] = None, + http_options: Optional[Any] = None, + ) -> Union[environments.GetEnvironmentFilesResponse, Any]: + """Uploads a file or extracts an archive inside an environment workspace.""" + if not self._api_client: + raise AttributeError('api_client is required to upload files.') + target_env = environment or environment_id + if not target_env: + raise ValueError('environment or environment_id is required.') + clean_env = ( + target_env.removeprefix('environments/') + if target_env.startswith('environments/') + else target_env + ) + clean_path = path.lstrip('/') + + file_obj: Union[str, io.IOBase] + if isinstance(file, (bytes, bytearray)): + file_obj = io.BytesIO(file) + size_bytes = len(file) + elif isinstance(file, io.IOBase): + file_obj = file + offset = file_obj.tell() + file_obj.seek(0, os.SEEK_END) + size_bytes = file_obj.tell() - offset + file_obj.seek(offset, os.SEEK_SET) + else: + fs_path = os.fspath(file) + if not fs_path or not os.path.isfile(fs_path): + raise FileNotFoundError(f'{file} is not a valid file path.') + size_bytes = os.path.getsize(fs_path) + file_obj = fs_path + if mime_type is None: + mime_type, _ = mimetypes.guess_type(fs_path) + + if mime_type is None: + mime_type = 'application/octet-stream' + + req_api_version = ( + ( + http_options.get('api_version') + if isinstance(http_options, dict) + else getattr(http_options, 'api_version', None) + ) + or self._api_client._http_options.api_version + or 'v1alpha' + ).lstrip('/') + + handshake_res = await self._internal.start_upload( + environment=clean_env, + path=clean_path, + x_goog_upload_header_content_length=size_bytes, + x_goog_upload_header_content_type=mime_type, + api_version=req_api_version, + extract=extract, + overwrite=overwrite, + ) + + upload_urls = ( + handshake_res.headers.get('x-goog-upload-url') + or handshake_res.headers.get('X-Goog-Upload-URL') + ) + if not upload_urls: + raise KeyError( + 'Failed to upload file: Upload URL was not returned from the upload request.' + ) + upload_url = upload_urls[0] if isinstance(upload_urls, list) else upload_urls + + upload_response = await self._api_client.async_upload_file( + file_obj, + upload_url, + size_bytes, + http_options=http_options, + ) + + body_text = ( + upload_response.response_stream[0] + if upload_response and upload_response.response_stream + else '{}' + ) + try: + res_json = json.loads(body_text) if body_text else {} + except Exception: + return body_text + + if isinstance(res_json, dict): + if 'files' in res_json and isinstance(res_json['files'], list): + return environments.GetEnvironmentFilesResponse.model_validate(res_json) + elif 'name' in res_json or 'path' in res_json: + file_obj = environments.EnvironmentFile.model_validate(res_json) + return environments.GetEnvironmentFilesResponse(files=[file_obj]) + elif 'file' in res_json and isinstance(res_json['file'], dict): + file_obj = environments.EnvironmentFile.model_validate(res_json['file']) + return environments.GetEnvironmentFilesResponse(files=[file_obj]) + return res_json + class GeminiNextGenEnvironments(GeneratedEnvironments): """Public environments resource backed by the NextGen client.""" diff --git a/google/genai/_gaos/models/__init__.py b/google/genai/_gaos/models/__init__.py index d1f924469..fa2c65963 100644 --- a/google/genai/_gaos/models/__init__.py +++ b/google/genai/_gaos/models/__init__.py @@ -202,6 +202,14 @@ RunTriggerRequest, RunTriggerRequestParam, ) + from .startenvironmentfileupload import ( + StartEnvironmentFileUploadGlobals, + StartEnvironmentFileUploadGlobalsTypedDict, + StartEnvironmentFileUploadRequest, + StartEnvironmentFileUploadRequestParam, + StartEnvironmentFileUploadResponse, + StartEnvironmentFileUploadResponseTypedDict, + ) from .updatecredential import ( UpdateCredentialGlobals, UpdateCredentialGlobalsTypedDict, @@ -345,6 +353,12 @@ "RunTriggerGlobalsTypedDict", "RunTriggerRequest", "RunTriggerRequestParam", + "StartEnvironmentFileUploadGlobals", + "StartEnvironmentFileUploadGlobalsTypedDict", + "StartEnvironmentFileUploadRequest", + "StartEnvironmentFileUploadRequestParam", + "StartEnvironmentFileUploadResponse", + "StartEnvironmentFileUploadResponseTypedDict", "UpdateCredentialGlobals", "UpdateCredentialGlobalsTypedDict", "UpdateCredentialRequest", @@ -482,6 +496,12 @@ "RunTriggerGlobalsTypedDict": ".runtrigger", "RunTriggerRequest": ".runtrigger", "RunTriggerRequestParam": ".runtrigger", + "StartEnvironmentFileUploadGlobals": ".startenvironmentfileupload", + "StartEnvironmentFileUploadGlobalsTypedDict": ".startenvironmentfileupload", + "StartEnvironmentFileUploadRequest": ".startenvironmentfileupload", + "StartEnvironmentFileUploadRequestParam": ".startenvironmentfileupload", + "StartEnvironmentFileUploadResponse": ".startenvironmentfileupload", + "StartEnvironmentFileUploadResponseTypedDict": ".startenvironmentfileupload", "UpdateCredentialGlobals": ".updatecredential", "UpdateCredentialGlobalsTypedDict": ".updatecredential", "UpdateCredentialRequest": ".updatecredential", diff --git a/google/genai/_gaos/models/startenvironmentfileupload.py b/google/genai/_gaos/models/startenvironmentfileupload.py new file mode 100644 index 000000000..534bd6973 --- /dev/null +++ b/google/genai/_gaos/models/startenvironmentfileupload.py @@ -0,0 +1,171 @@ +# 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 +# pylint: skip-file + +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from ..types import BaseModel, UNSET_SENTINEL +from ..utils import ( + FieldMetadata, + HeaderMetadata, + PathParamMetadata, + QueryParamMetadata, + validate_const, +) +import pydantic +from pydantic import model_serializer +from pydantic.functional_validators import AfterValidator +from typing import Dict, List, Literal, Optional +from typing_extensions import Annotated, NotRequired, TypedDict + + +class StartEnvironmentFileUploadGlobalsTypedDict(TypedDict): + api_version: NotRequired[str] + r"""Which version of the API to use.""" + + +class StartEnvironmentFileUploadGlobals(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 StartEnvironmentFileUploadRequestParam(TypedDict): + environment: str + r"""The ID of the environment that owns the destination file.""" + path: str + r"""The relative destination path inside the environment workspace.""" + x_goog_upload_header_content_length: int + r"""Total number of file bytes that will be uploaded to the session URL.""" + x_goog_upload_header_content_type: str + r"""MIME type of the file that will be uploaded to the session URL.""" + api_version: NotRequired[str] + r"""Which version of the API to use.""" + extract: NotRequired[bool] + r"""Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`.""" + overwrite: NotRequired[bool] + r"""Optional. Whether to overwrite the destination file if it already exists.""" + x_goog_upload_command: Literal["start"] + r"""Command that starts the resumable upload session.""" + x_goog_upload_protocol: Literal["resumable"] + r"""Resumable upload protocol selector.""" + + +class StartEnvironmentFileUploadRequest(BaseModel): + environment: Annotated[ + str, FieldMetadata(path=PathParamMetadata(style="simple", explode=False)) + ] + r"""The ID of the environment that owns the destination file.""" + + path: Annotated[ + str, FieldMetadata(path=PathParamMetadata(style="simple", explode=False)) + ] + r"""The relative destination path inside the environment workspace.""" + + x_goog_upload_header_content_length: Annotated[ + int, + pydantic.Field(alias="X-Goog-Upload-Header-Content-Length"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] + r"""Total number of file bytes that will be uploaded to the session URL.""" + + x_goog_upload_header_content_type: Annotated[ + str, + pydantic.Field(alias="X-Goog-Upload-Header-Content-Type"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] + r"""MIME type of the file that will be uploaded to the session URL.""" + + api_version: Annotated[ + Optional[str], + FieldMetadata(path=PathParamMetadata(style="simple", explode=False)), + ] = None + r"""Which version of the API to use.""" + + extract: Annotated[ + Optional[bool], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""Optional. If true, treats the uploaded file as a tar/tar.gz archive and unpacks it into `path`.""" + + overwrite: Annotated[ + Optional[bool], + FieldMetadata(query=QueryParamMetadata(style="form", explode=True)), + ] = None + r"""Optional. Whether to overwrite the destination file if it already exists.""" + + x_goog_upload_command: Annotated[ + Annotated[Literal["start"], AfterValidator(validate_const("start"))], + pydantic.Field(alias="X-Goog-Upload-Command"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] = "start" + r"""Command that starts the resumable upload session.""" + + x_goog_upload_protocol: Annotated[ + Annotated[Literal["resumable"], AfterValidator(validate_const("resumable"))], + pydantic.Field(alias="X-Goog-Upload-Protocol"), + FieldMetadata(header=HeaderMetadata(style="simple", explode=False)), + ] = "resumable" + r"""Resumable upload protocol selector.""" + + @model_serializer(mode="wrap") + def serialize_model(self, handler): + optional_fields = set(["api_version", "extract", "overwrite"]) + 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 StartEnvironmentFileUploadResponseTypedDict(TypedDict): + headers: Dict[str, List[str]] + + +class StartEnvironmentFileUploadResponse(BaseModel): + headers: Dict[str, List[str]] + + +try: + StartEnvironmentFileUploadRequest.model_rebuild() +except NameError: + pass diff --git a/google/genai/_gaos/sdk.py b/google/genai/_gaos/sdk.py index 5ff3c28c3..ecc566eae 100644 --- a/google/genai/_gaos/sdk.py +++ b/google/genai/_gaos/sdk.py @@ -58,16 +58,16 @@ def with_raw_response(self): def with_streaming_response(self): return GenAIWithStreamingResponse(self) + environments: "Environments" agents: "Agents" credentials: "Credentials" - environments: "Environments" interactions: "Interactions" triggers: "Triggers" webhooks: "Webhooks" _sub_sdk_map = { + "environments": (".environments", "Environments"), "agents": (".agents", "Agents"), "credentials": (".credentials", "Credentials"), - "environments": (".environments", "Environments"), "interactions": (".interactions", "Interactions"), "triggers": (".triggers", "Triggers"), "webhooks": (".webhooks", "Webhooks"), @@ -214,6 +214,10 @@ class GenAIWithRawResponse: def __init__(self, sdk: GenAI) -> None: self._sdk = sdk + @property + def environments(self): + return self._sdk.environments.with_raw_response + @property def agents(self): return self._sdk.agents.with_raw_response @@ -222,10 +226,6 @@ def agents(self): def credentials(self): return self._sdk.credentials.with_raw_response - @property - def environments(self): - return self._sdk.environments.with_raw_response - @property def interactions(self): return self._sdk.interactions.with_raw_response @@ -243,6 +243,10 @@ class GenAIWithStreamingResponse: def __init__(self, sdk: GenAI) -> None: self._sdk = sdk + @property + def environments(self): + return self._sdk.environments.with_streaming_response + @property def agents(self): return self._sdk.agents.with_streaming_response @@ -251,10 +255,6 @@ def agents(self): def credentials(self): return self._sdk.credentials.with_streaming_response - @property - def environments(self): - return self._sdk.environments.with_streaming_response - @property def interactions(self): return self._sdk.interactions.with_streaming_response @@ -279,16 +279,16 @@ def with_raw_response(self): def with_streaming_response(self): return AsyncGenAIWithStreamingResponse(self) + environments: "AsyncEnvironments" agents: "AsyncAgents" credentials: "AsyncCredentials" - environments: "AsyncEnvironments" interactions: "AsyncInteractions" triggers: "AsyncTriggers" webhooks: "AsyncWebhooks" _sub_sdk_map = { + "environments": (".environments", "AsyncEnvironments"), "agents": (".agents", "AsyncAgents"), "credentials": (".credentials", "AsyncCredentials"), - "environments": (".environments", "AsyncEnvironments"), "interactions": (".interactions", "AsyncInteractions"), "triggers": (".triggers", "AsyncTriggers"), "webhooks": (".webhooks", "AsyncWebhooks"), @@ -433,6 +433,10 @@ class AsyncGenAIWithRawResponse: def __init__(self, sdk: AsyncGenAI) -> None: self._sdk = sdk + @property + def environments(self): + return self._sdk.environments.with_raw_response + @property def agents(self): return self._sdk.agents.with_raw_response @@ -441,10 +445,6 @@ def agents(self): def credentials(self): return self._sdk.credentials.with_raw_response - @property - def environments(self): - return self._sdk.environments.with_raw_response - @property def interactions(self): return self._sdk.interactions.with_raw_response @@ -462,6 +462,10 @@ class AsyncGenAIWithStreamingResponse: def __init__(self, sdk: AsyncGenAI) -> None: self._sdk = sdk + @property + def environments(self): + return self._sdk.environments.with_streaming_response + @property def agents(self): return self._sdk.agents.with_streaming_response @@ -470,10 +474,6 @@ def agents(self): def credentials(self): return self._sdk.credentials.with_streaming_response - @property - def environments(self): - return self._sdk.environments.with_streaming_response - @property def interactions(self): return self._sdk.interactions.with_streaming_response diff --git a/google/genai/tests/gaos/test_environments_lifecycle.py b/google/genai/tests/gaos/test_environments_lifecycle.py index 39cb3c90c..c599f965b 100644 --- a/google/genai/tests/gaos/test_environments_lifecycle.py +++ b/google/genai/tests/gaos/test_environments_lifecycle.py @@ -98,6 +98,8 @@ def test_python_environments_lifecycle_routes_through_google_genai_client( monkeypatch, ): monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + for var in ("http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + monkeypatch.delenv(var, raising=False) captured: list[str] = [] captured_bodies: list[dict] = [] handler = type("Handler", (_RecordingHandler,), { @@ -195,8 +197,49 @@ async def test_python_environments_async_create_with_from_environment( server.server_close() -class _ScottyDownloadHandler(BaseHTTPRequestHandler): +class _ScottyFileHandler(BaseHTTPRequestHandler): captured: list[str] = [] + uploaded_bytes: list[bytes] = [] + + def do_PUT(self) -> None: + self.captured.append(f"PUT {self.path}") + if self.path.startswith("/upload/") and ("/environments/" in self.path) and ("/files/" in self.path): + # Initial Scotty upload handshake + upload_url = f"http://127.0.0.1:{self.server.server_port}/scotty/upload/resumable_123" + self.send_response(200) + self.send_header("x-goog-upload-url", upload_url) + self.send_header("x-goog-upload-status", "active") + self.send_header("content-length", "0") + self.end_headers() + return + + self.send_response(404) + self.end_headers() + + def do_POST(self) -> None: + self.captured.append(f"POST {self.path}") + if self.path == "/scotty/upload/resumable_123": + content_length = int(self.headers.get("Content-Length", 0)) + data = self.rfile.read(content_length) + self.uploaded_bytes.append(data) + file_response = { + "file": { + "name": "main.py", + "sizeBytes": str(len(data)), + "mimeType": "text/x-python", + } + } + payload = json.dumps(file_response).encode() + self.send_response(200) + self.send_header("content-type", "application/json") + self.send_header("x-goog-upload-status", "final") + self.send_header("content-length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + return + + self.send_response(404) + self.end_headers() def do_GET(self) -> None: self.captured.append(f"GET {self.path}") @@ -224,11 +267,15 @@ def log_message(self, *args) -> None: pass -def test_python_environments_files_list_and_download(monkeypatch): +def test_python_environments_file_upload_download(monkeypatch): monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + for var in ("http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + monkeypatch.delenv(var, raising=False) captured: list[str] = [] - handler = type("Handler", (_ScottyDownloadHandler,), { + uploaded_bytes: list[bytes] = [] + handler = type("Handler", (_ScottyFileHandler,), { "captured": captured, + "uploaded_bytes": uploaded_bytes, }) server = ThreadingHTTPServer(("127.0.0.1", 0), handler) thread = threading.Thread(target=server.serve_forever, daemon=True) @@ -255,6 +302,7 @@ def test_python_environments_files_list_and_download(monkeypatch): assert files_res.files[0].size_bytes == 128 assert files_res.next_page_token == "token_next_123" + # Test sync files.list with pagination and recursive options files_res_paginated = client.environments.files.list( environment="env_123", @@ -265,6 +313,17 @@ def test_python_environments_files_list_and_download(monkeypatch): ) assert len(files_res_paginated.files) == 1 + # Test sync upload + upload_res = client.environments.files.upload( + environment="env_123", + path="src/main.py", + file=b"print('hello world')", + mime_type="text/x-python", + ) + assert upload_res.files and len(upload_res.files) == 1 + assert upload_res.files[0].name == "main.py" + assert uploaded_bytes[0] == b"print('hello world')" + # Test sync files.download downloaded = client.environments.files.download( environment="env_123", @@ -299,11 +358,15 @@ def test_python_environments_files_list_and_download(monkeypatch): @pytest.mark.asyncio -async def test_python_environments_async_files_list_and_download(monkeypatch): +async def test_python_environments_async_file_upload_download(monkeypatch): monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + for var in ("http_proxy", "https_proxy", "all_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"): + monkeypatch.delenv(var, raising=False) captured: list[str] = [] - handler = type("Handler", (_ScottyDownloadHandler,), { + uploaded_bytes: list[bytes] = [] + handler = type("Handler", (_ScottyFileHandler,), { "captured": captured, + "uploaded_bytes": uploaded_bytes, }) server = ThreadingHTTPServer(("127.0.0.1", 0), handler) thread = threading.Thread(target=server.serve_forever, daemon=True) @@ -314,6 +377,7 @@ async def test_python_environments_async_files_list_and_download(monkeypatch): http_options={ "api_version": "v1beta", "base_url": f"http://127.0.0.1:{server.server_port}", + "headers": {"X-Goog-Api-Client": "test"}, }, ) @@ -338,6 +402,17 @@ async def test_python_environments_async_files_list_and_download(monkeypatch): ) assert len(files_res_paginated.files) == 1 + # Test async upload + upload_res = await client.aio.environments.files.upload( + environment="env_123", + path="src/main.py", + file=b"print('async hello world')", + mime_type="text/x-python", + ) + assert upload_res.files and len(upload_res.files) == 1 + assert upload_res.files[0].name == "main.py" + assert uploaded_bytes[0] == b"print('async hello world')" + # Test async files.download downloaded = await client.aio.environments.files.download( environment="env_123",