Skip to content

Commit 1187d82

Browse files
handle missing auth case for register_files
1 parent ee23e51 commit 1187d82

3 files changed

Lines changed: 62 additions & 8 deletions

File tree

temporalio/contrib/google_gemini_sdk/_gemini_activity.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,15 @@ async def gemini_files_register(
130130
Token refresh happens here on the worker side, so no auth
131131
material enters the workflow event history.
132132
"""
133+
auth = self._credentials or self._client._api_client._credentials
134+
if auth is None:
135+
raise ValueError(
136+
"No credentials available for register_files(). "
137+
"Pass extra_credentials to GeminiPlugin or initialize "
138+
"the genai.Client with credentials."
139+
)
133140
return await self._client.aio.files.register_files(
134-
auth=self._credentials or self._client._api_client._credentials,
141+
auth=auth,
135142
uris=req.uris,
136143
config=req.config,
137144
)

temporalio/contrib/google_gemini_sdk/_temporal_files.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
import io
1212
import os
1313
from datetime import timedelta
14-
from typing import Optional, Union
14+
from typing import TYPE_CHECKING, Optional, Union
1515

16-
import google.auth.credentials
16+
if TYPE_CHECKING:
17+
import google.auth.credentials
1718
from google.genai import types
1819
from google.genai.files import AsyncFiles
1920
from google.genai.types import HttpOptions

tests/contrib/google_gemini_sdk/test_gemini.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ def apply_plugin(
257257

258258
tracker = GeminiApiCallTracker(mock_responses)
259259
original_activities = GeminiApiCaller.activities
260-
GeminiApiCaller.activities = lambda self: [
260+
GeminiApiCaller.activities = lambda self: [ # type: ignore[method-assign]
261261
tracker.gemini_api_client_async_request,
262262
tracker.gemini_api_client_async_request_streamed,
263263
tracker.gemini_files_upload,
@@ -268,7 +268,7 @@ def apply_plugin(
268268
gemini = GeminiClient(api_key="fake-test-key")
269269
plugin = GeminiPlugin(gemini)
270270
finally:
271-
GeminiApiCaller.activities = original_activities
271+
GeminiApiCaller.activities = original_activities # type: ignore[method-assign]
272272

273273
config = client.config()
274274
config["plugins"] = [plugin]
@@ -598,6 +598,24 @@ async def run(self, store_name: str, file_path: str) -> str:
598598
return op.name or ""
599599

600600

601+
@workflow.defn
602+
class RegisterFilesWorkflow:
603+
"""Workflow that calls files.register_files."""
604+
605+
@workflow.run
606+
async def run(self, uris: list[str]) -> str:
607+
client = gemini_client()
608+
# auth arg is ignored by TemporalAsyncFiles — the activity uses
609+
# credentials from GeminiPlugin init. We pass a dummy here;
610+
# can't import google.auth.credentials in the sandbox so we
611+
# use a sentinel that satisfies the type at runtime.
612+
resp = await client.files.register_files(
613+
auth=None, # type: ignore[arg-type]
614+
uris=uris,
615+
)
616+
return str(len(resp.files or []))
617+
618+
601619
@workflow.defn
602620
class ChatWorkflow:
603621
"""Workflow that uses client.chats for multi-turn conversation."""
@@ -1095,15 +1113,15 @@ async def _gen():
10951113

10961114
# Mock file operations at the high-level SDK interface (these are what
10971115
# the real activities call).
1098-
gemini.aio.files.upload = AsyncMock(
1116+
gemini.aio.files.upload = AsyncMock( # type: ignore[method-assign]
10991117
return_value=types.File(
11001118
name="files/mock-uploaded",
11011119
uri="https://fake.uri/files/mock-uploaded",
11021120
size_bytes=42,
11031121
)
11041122
)
1105-
gemini.aio.files.download = AsyncMock(return_value=b"mock download content")
1106-
gemini.aio.file_search_stores.upload_to_file_search_store = AsyncMock(
1123+
gemini.aio.files.download = AsyncMock(return_value=b"mock download content") # type: ignore[method-assign]
1124+
gemini.aio.file_search_stores.upload_to_file_search_store = AsyncMock( # type: ignore[method-assign]
11071125
return_value=types.UploadToFileSearchStoreOperation.model_construct(
11081126
name="operations/mock-op"
11091127
)
@@ -1155,6 +1173,34 @@ async def test_full_integration_with_mock_client(client: Client):
11551173
assert result["store_deleted"] is True
11561174

11571175

1176+
async def test_register_files_without_credentials_fails(client: Client):
1177+
"""register_files raises when no credentials are available."""
1178+
# _apply_plugin_with_mock_client uses api_key auth with no
1179+
# extra_credentials, so the activity should raise ValueError.
1180+
new_client = _apply_plugin_with_mock_client(client, [])
1181+
1182+
async with new_worker(new_client, RegisterFilesWorkflow) as worker:
1183+
with pytest.raises(WorkflowFailureError) as exc_info:
1184+
await new_client.execute_workflow(
1185+
RegisterFilesWorkflow.run,
1186+
["gs://bucket/file.txt"],
1187+
id=f"gemini-register-no-creds-{uuid.uuid4()}",
1188+
task_queue=worker.task_queue,
1189+
execution_timeout=timedelta(seconds=10),
1190+
)
1191+
1192+
# The error is nested: WorkflowFailureError → ActivityError → ApplicationError
1193+
cause: BaseException | None = exc_info.value.cause
1194+
while (
1195+
cause is not None
1196+
and hasattr(cause, "__cause__")
1197+
and cause.__cause__ is not None
1198+
):
1199+
cause = cause.__cause__
1200+
assert cause is not None
1201+
assert "No credentials available for register_files" in str(cause)
1202+
1203+
11581204
# ===========================================================================
11591205
# TemporalAsyncClient wiring tests
11601206
# ===========================================================================

0 commit comments

Comments
 (0)