veadk-python 0.2.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- veadk/__init__.py +37 -0
- veadk/a2a/__init__.py +13 -0
- veadk/a2a/agent_card.py +45 -0
- veadk/a2a/remote_ve_agent.py +390 -0
- veadk/a2a/utils/__init__.py +13 -0
- veadk/a2a/utils/agent_to_a2a.py +170 -0
- veadk/a2a/ve_a2a_server.py +93 -0
- veadk/a2a/ve_agent_executor.py +78 -0
- veadk/a2a/ve_middlewares.py +313 -0
- veadk/a2a/ve_task_store.py +37 -0
- veadk/agent.py +402 -0
- veadk/agent_builder.py +93 -0
- veadk/agents/loop_agent.py +68 -0
- veadk/agents/parallel_agent.py +72 -0
- veadk/agents/sequential_agent.py +64 -0
- veadk/auth/__init__.py +13 -0
- veadk/auth/base_auth.py +22 -0
- veadk/auth/ve_credential_service.py +203 -0
- veadk/auth/veauth/__init__.py +13 -0
- veadk/auth/veauth/apmplus_veauth.py +58 -0
- veadk/auth/veauth/ark_veauth.py +75 -0
- veadk/auth/veauth/base_veauth.py +50 -0
- veadk/auth/veauth/cozeloop_veauth.py +13 -0
- veadk/auth/veauth/opensearch_veauth.py +75 -0
- veadk/auth/veauth/postgresql_veauth.py +75 -0
- veadk/auth/veauth/prompt_pilot_veauth.py +60 -0
- veadk/auth/veauth/speech_veauth.py +54 -0
- veadk/auth/veauth/utils.py +69 -0
- veadk/auth/veauth/vesearch_veauth.py +62 -0
- veadk/auth/veauth/viking_mem0_veauth.py +91 -0
- veadk/cli/__init__.py +13 -0
- veadk/cli/cli.py +58 -0
- veadk/cli/cli_clean.py +87 -0
- veadk/cli/cli_create.py +163 -0
- veadk/cli/cli_deploy.py +233 -0
- veadk/cli/cli_eval.py +215 -0
- veadk/cli/cli_init.py +214 -0
- veadk/cli/cli_kb.py +110 -0
- veadk/cli/cli_pipeline.py +285 -0
- veadk/cli/cli_prompt.py +86 -0
- veadk/cli/cli_update.py +106 -0
- veadk/cli/cli_uploadevalset.py +139 -0
- veadk/cli/cli_web.py +143 -0
- veadk/cloud/__init__.py +13 -0
- veadk/cloud/cloud_agent_engine.py +485 -0
- veadk/cloud/cloud_app.py +475 -0
- veadk/config.py +115 -0
- veadk/configs/__init__.py +13 -0
- veadk/configs/auth_configs.py +133 -0
- veadk/configs/database_configs.py +132 -0
- veadk/configs/model_configs.py +78 -0
- veadk/configs/tool_configs.py +54 -0
- veadk/configs/tracing_configs.py +110 -0
- veadk/consts.py +74 -0
- veadk/evaluation/__init__.py +17 -0
- veadk/evaluation/adk_evaluator/__init__.py +17 -0
- veadk/evaluation/adk_evaluator/adk_evaluator.py +302 -0
- veadk/evaluation/base_evaluator.py +642 -0
- veadk/evaluation/deepeval_evaluator/__init__.py +17 -0
- veadk/evaluation/deepeval_evaluator/deepeval_evaluator.py +339 -0
- veadk/evaluation/eval_set_file_loader.py +48 -0
- veadk/evaluation/eval_set_recorder.py +146 -0
- veadk/evaluation/types.py +65 -0
- veadk/evaluation/utils/prometheus.py +196 -0
- veadk/integrations/__init__.py +13 -0
- veadk/integrations/ve_apig/__init__.py +13 -0
- veadk/integrations/ve_apig/ve_apig.py +349 -0
- veadk/integrations/ve_apig/ve_apig_utils.py +332 -0
- veadk/integrations/ve_code_pipeline/__init__.py +13 -0
- veadk/integrations/ve_code_pipeline/ve_code_pipeline.py +431 -0
- veadk/integrations/ve_cozeloop/__init__.py +13 -0
- veadk/integrations/ve_cozeloop/ve_cozeloop.py +96 -0
- veadk/integrations/ve_cr/__init__.py +13 -0
- veadk/integrations/ve_cr/ve_cr.py +220 -0
- veadk/integrations/ve_faas/__init__.py +13 -0
- veadk/integrations/ve_faas/template/cookiecutter.json +15 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/__init__.py +13 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/clean.py +23 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/config.yaml.example +6 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/deploy.py +106 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/__init__.py +13 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/agent.py +25 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/app.py +202 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/requirements.txt +3 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/run.sh +49 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/{{ cookiecutter.app_name }}/__init__.py +14 -0
- veadk/integrations/ve_faas/template/{{cookiecutter.local_dir_name}}/src/{{ cookiecutter.app_name }}/agent.py +27 -0
- veadk/integrations/ve_faas/ve_faas.py +754 -0
- veadk/integrations/ve_faas/ve_faas_utils.py +408 -0
- veadk/integrations/ve_faas/web_template/cookiecutter.json +20 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/__init__.py +13 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/clean.py +23 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/config.yaml.example +2 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/deploy.py +44 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/Dockerfile +23 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/app.py +123 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/init_db.py +46 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/models.py +36 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/requirements.txt +4 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/run.sh +21 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/static/css/style.css +368 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/static/js/admin.js +0 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/templates/admin/dashboard.html +21 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/templates/admin/edit_post.html +24 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/templates/admin/login.html +21 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/templates/admin/posts.html +53 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/templates/base.html +45 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/templates/index.html +29 -0
- veadk/integrations/ve_faas/web_template/{{cookiecutter.local_dir_name}}/src/templates/post.html +14 -0
- veadk/integrations/ve_identity/__init__.py +110 -0
- veadk/integrations/ve_identity/auth_config.py +261 -0
- veadk/integrations/ve_identity/auth_mixins.py +650 -0
- veadk/integrations/ve_identity/auth_processor.py +385 -0
- veadk/integrations/ve_identity/function_tool.py +158 -0
- veadk/integrations/ve_identity/identity_client.py +864 -0
- veadk/integrations/ve_identity/mcp_tool.py +181 -0
- veadk/integrations/ve_identity/mcp_toolset.py +431 -0
- veadk/integrations/ve_identity/models.py +228 -0
- veadk/integrations/ve_identity/token_manager.py +188 -0
- veadk/integrations/ve_identity/utils.py +151 -0
- veadk/integrations/ve_prompt_pilot/__init__.py +13 -0
- veadk/integrations/ve_prompt_pilot/ve_prompt_pilot.py +85 -0
- veadk/integrations/ve_tls/__init__.py +13 -0
- veadk/integrations/ve_tls/utils.py +116 -0
- veadk/integrations/ve_tls/ve_tls.py +212 -0
- veadk/integrations/ve_tos/ve_tos.py +710 -0
- veadk/integrations/ve_viking_db_memory/__init__.py +13 -0
- veadk/integrations/ve_viking_db_memory/ve_viking_db_memory.py +308 -0
- veadk/knowledgebase/__init__.py +17 -0
- veadk/knowledgebase/backends/__init__.py +13 -0
- veadk/knowledgebase/backends/base_backend.py +72 -0
- veadk/knowledgebase/backends/in_memory_backend.py +91 -0
- veadk/knowledgebase/backends/opensearch_backend.py +162 -0
- veadk/knowledgebase/backends/redis_backend.py +172 -0
- veadk/knowledgebase/backends/utils.py +92 -0
- veadk/knowledgebase/backends/vikingdb_knowledge_backend.py +608 -0
- veadk/knowledgebase/entry.py +25 -0
- veadk/knowledgebase/knowledgebase.py +307 -0
- veadk/memory/__init__.py +35 -0
- veadk/memory/long_term_memory.py +365 -0
- veadk/memory/long_term_memory_backends/__init__.py +13 -0
- veadk/memory/long_term_memory_backends/base_backend.py +35 -0
- veadk/memory/long_term_memory_backends/in_memory_backend.py +67 -0
- veadk/memory/long_term_memory_backends/mem0_backend.py +155 -0
- veadk/memory/long_term_memory_backends/opensearch_backend.py +124 -0
- veadk/memory/long_term_memory_backends/redis_backend.py +140 -0
- veadk/memory/long_term_memory_backends/vikingdb_memory_backend.py +189 -0
- veadk/memory/short_term_memory.py +252 -0
- veadk/memory/short_term_memory_backends/__init__.py +13 -0
- veadk/memory/short_term_memory_backends/base_backend.py +31 -0
- veadk/memory/short_term_memory_backends/mysql_backend.py +49 -0
- veadk/memory/short_term_memory_backends/postgresql_backend.py +49 -0
- veadk/memory/short_term_memory_backends/sqlite_backend.py +55 -0
- veadk/memory/short_term_memory_processor.py +100 -0
- veadk/processors/__init__.py +26 -0
- veadk/processors/base_run_processor.py +120 -0
- veadk/prompts/__init__.py +13 -0
- veadk/prompts/agent_default_prompt.py +30 -0
- veadk/prompts/prompt_evaluator.py +20 -0
- veadk/prompts/prompt_memory_processor.py +55 -0
- veadk/prompts/prompt_optimization.py +150 -0
- veadk/runner.py +732 -0
- veadk/tools/__init__.py +13 -0
- veadk/tools/builtin_tools/__init__.py +13 -0
- veadk/tools/builtin_tools/agent_authorization.py +94 -0
- veadk/tools/builtin_tools/generate_image.py +23 -0
- veadk/tools/builtin_tools/image_edit.py +300 -0
- veadk/tools/builtin_tools/image_generate.py +446 -0
- veadk/tools/builtin_tools/lark.py +67 -0
- veadk/tools/builtin_tools/las.py +24 -0
- veadk/tools/builtin_tools/link_reader.py +66 -0
- veadk/tools/builtin_tools/llm_shield.py +381 -0
- veadk/tools/builtin_tools/load_knowledgebase.py +97 -0
- veadk/tools/builtin_tools/mcp_router.py +29 -0
- veadk/tools/builtin_tools/run_code.py +113 -0
- veadk/tools/builtin_tools/tts.py +253 -0
- veadk/tools/builtin_tools/vesearch.py +49 -0
- veadk/tools/builtin_tools/video_generate.py +363 -0
- veadk/tools/builtin_tools/web_scraper.py +76 -0
- veadk/tools/builtin_tools/web_search.py +83 -0
- veadk/tools/demo_tools.py +58 -0
- veadk/tools/load_knowledgebase_tool.py +149 -0
- veadk/tools/sandbox/__init__.py +13 -0
- veadk/tools/sandbox/browser_sandbox.py +37 -0
- veadk/tools/sandbox/code_sandbox.py +40 -0
- veadk/tools/sandbox/computer_sandbox.py +34 -0
- veadk/tracing/__init__.py +13 -0
- veadk/tracing/base_tracer.py +58 -0
- veadk/tracing/telemetry/__init__.py +13 -0
- veadk/tracing/telemetry/attributes/attributes.py +29 -0
- veadk/tracing/telemetry/attributes/extractors/common_attributes_extractors.py +180 -0
- veadk/tracing/telemetry/attributes/extractors/llm_attributes_extractors.py +858 -0
- veadk/tracing/telemetry/attributes/extractors/tool_attributes_extractors.py +152 -0
- veadk/tracing/telemetry/attributes/extractors/types.py +164 -0
- veadk/tracing/telemetry/exporters/__init__.py +13 -0
- veadk/tracing/telemetry/exporters/apmplus_exporter.py +558 -0
- veadk/tracing/telemetry/exporters/base_exporter.py +39 -0
- veadk/tracing/telemetry/exporters/cozeloop_exporter.py +129 -0
- veadk/tracing/telemetry/exporters/inmemory_exporter.py +248 -0
- veadk/tracing/telemetry/exporters/tls_exporter.py +139 -0
- veadk/tracing/telemetry/opentelemetry_tracer.py +320 -0
- veadk/tracing/telemetry/telemetry.py +411 -0
- veadk/types.py +47 -0
- veadk/utils/__init__.py +13 -0
- veadk/utils/audio_manager.py +95 -0
- veadk/utils/auth.py +294 -0
- veadk/utils/logger.py +59 -0
- veadk/utils/mcp_utils.py +44 -0
- veadk/utils/misc.py +184 -0
- veadk/utils/patches.py +101 -0
- veadk/utils/volcengine_sign.py +205 -0
- veadk/version.py +15 -0
- veadk_python-0.2.27.dist-info/METADATA +373 -0
- veadk_python-0.2.27.dist-info/RECORD +218 -0
- veadk_python-0.2.27.dist-info/WHEEL +5 -0
- veadk_python-0.2.27.dist-info/entry_points.txt +2 -0
- veadk_python-0.2.27.dist-info/licenses/LICENSE +201 -0
- veadk_python-0.2.27.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Authentication mixins for Identity integration.
|
|
17
|
+
|
|
18
|
+
These mixins provide reusable authentication logic that can be mixed into
|
|
19
|
+
different tool classes to avoid code duplication.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import urllib.parse
|
|
25
|
+
import asyncio
|
|
26
|
+
from typing import Any, Callable, List, Literal, Optional
|
|
27
|
+
from abc import ABC, abstractmethod
|
|
28
|
+
|
|
29
|
+
from google.adk.auth.auth_credential import (
|
|
30
|
+
AuthCredential,
|
|
31
|
+
AuthCredentialTypes,
|
|
32
|
+
OAuth2Auth,
|
|
33
|
+
)
|
|
34
|
+
from google.adk.auth.auth_tool import AuthConfig
|
|
35
|
+
from google.adk.auth.auth_handler import AuthHandler
|
|
36
|
+
from google.adk.tools.tool_context import ToolContext
|
|
37
|
+
from google.adk.tools.openapi_tool.auth.auth_helpers import dict_to_auth_scheme
|
|
38
|
+
from google.adk.agents.readonly_context import ReadonlyContext
|
|
39
|
+
|
|
40
|
+
from veadk.integrations.ve_identity.models import OAuth2AuthPoller, OAuth2TokenResponse
|
|
41
|
+
from veadk.integrations.ve_identity.identity_client import IdentityClient
|
|
42
|
+
from veadk.integrations.ve_identity.auth_config import (
|
|
43
|
+
VeIdentityAuthConfig,
|
|
44
|
+
ApiKeyAuthConfig,
|
|
45
|
+
OAuth2AuthConfig,
|
|
46
|
+
WorkloadAuthConfig,
|
|
47
|
+
get_default_identity_client,
|
|
48
|
+
)
|
|
49
|
+
from veadk.integrations.ve_identity.token_manager import get_workload_token
|
|
50
|
+
|
|
51
|
+
from veadk.utils.logger import get_logger
|
|
52
|
+
|
|
53
|
+
logger = get_logger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# OAuth2 scheme definition (shared across all OAuth2 tools)
|
|
57
|
+
oauth2_scheme = dict_to_auth_scheme(
|
|
58
|
+
{
|
|
59
|
+
"type": "oauth2",
|
|
60
|
+
"flows": {
|
|
61
|
+
"authorizationCode": {
|
|
62
|
+
"authorizationUrl": "__EMPTY__",
|
|
63
|
+
"tokenUrl": "__EMPTY__",
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AuthRequiredException(Exception):
|
|
71
|
+
"""Exception raised when user authorization is required for OAuth2 flow."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, message: str):
|
|
74
|
+
self.message = message
|
|
75
|
+
super().__init__(message)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BaseAuthMixin(ABC):
|
|
79
|
+
"""Base mixin for Identity authentication.
|
|
80
|
+
|
|
81
|
+
This mixin provides common functionality for all Identity authentication types.
|
|
82
|
+
Specific authentication implementations should inherit from this class.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
provider_name: str,
|
|
89
|
+
identity_client: Optional[IdentityClient] = None,
|
|
90
|
+
region: Optional[str] = None,
|
|
91
|
+
**kwargs,
|
|
92
|
+
):
|
|
93
|
+
"""Initialize the Identity authentication mixin.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
provider_name: Name of the credential provider configured in identity service.
|
|
97
|
+
identity_client: Optional IdentityClient instance. If not provided, creates a new one.
|
|
98
|
+
region: VolcEngine region for the identity client. If not provided, uses the region
|
|
99
|
+
from VeADK config. Defaults to "cn-beijing" if config is not available.
|
|
100
|
+
**kwargs: Additional arguments passed to parent classes.
|
|
101
|
+
"""
|
|
102
|
+
# Only pass kwargs to super() if we're in a multiple inheritance scenario
|
|
103
|
+
# and the next class in MRO expects them
|
|
104
|
+
try:
|
|
105
|
+
super().__init__(**kwargs)
|
|
106
|
+
except TypeError:
|
|
107
|
+
# If super().__init__() doesn't accept kwargs (e.g., object.__init__),
|
|
108
|
+
# call it without arguments
|
|
109
|
+
super().__init__()
|
|
110
|
+
|
|
111
|
+
self._identity_client = identity_client or get_default_identity_client(region)
|
|
112
|
+
self._provider_name = provider_name
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
async def _get_credential(
|
|
116
|
+
self, *, tool_context: ToolContext | ReadonlyContext
|
|
117
|
+
) -> AuthCredential:
|
|
118
|
+
"""Get or create authentication credential.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
tool_context: The tool context for accessing session state.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The authentication credential.
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def _execute_with_credential(
|
|
130
|
+
self,
|
|
131
|
+
*,
|
|
132
|
+
args: dict[str, Any],
|
|
133
|
+
tool_context: ToolContext | ReadonlyContext,
|
|
134
|
+
credential: AuthCredential,
|
|
135
|
+
) -> Any:
|
|
136
|
+
"""Execute the tool with the provided credential.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
args: Arguments to pass to the tool.
|
|
140
|
+
tool_context: The tool context.
|
|
141
|
+
credential: The authentication credential.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The result from the tool execution.
|
|
145
|
+
"""
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
async def run_with_identity_auth(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
args: dict[str, Any],
|
|
152
|
+
tool_context: ToolContext | ReadonlyContext,
|
|
153
|
+
) -> Any:
|
|
154
|
+
"""Execute the tool with Identity authentication.
|
|
155
|
+
|
|
156
|
+
This is the main entry point that handles the authentication flow
|
|
157
|
+
and delegates to the specific implementation.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
args: Arguments to pass to the tool.
|
|
161
|
+
tool_context: The tool context.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The result from the tool execution.
|
|
165
|
+
"""
|
|
166
|
+
credential = await self._get_credential(tool_context=tool_context)
|
|
167
|
+
return await self._execute_with_credential(
|
|
168
|
+
args=args, tool_context=tool_context, credential=credential
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ApiKeyAuthMixin(BaseAuthMixin):
|
|
173
|
+
"""Mixin for API key authentication via Identity Service.
|
|
174
|
+
|
|
175
|
+
This mixin handles:
|
|
176
|
+
1. Retrieving workload access tokens for the agent
|
|
177
|
+
2. Fetching API keys from the identity service
|
|
178
|
+
3. Caching API keys in session state
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
async def _get_credential(
|
|
182
|
+
self, *, tool_context: ToolContext | ReadonlyContext
|
|
183
|
+
) -> AuthCredential:
|
|
184
|
+
"""Get or create API key credential.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
tool_context: The tool context for accessing session state.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
The API key credential.
|
|
191
|
+
"""
|
|
192
|
+
# Build cache key for the API key
|
|
193
|
+
credential_key = (
|
|
194
|
+
f"api_key:{tool_context.agent_name}:"
|
|
195
|
+
f"{tool_context._invocation_context.user_id}:{self._provider_name}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Try to get cached API key
|
|
199
|
+
credential: AuthCredential = tool_context._invocation_context.session.state.get(
|
|
200
|
+
credential_key
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Fetch API key if not cached
|
|
204
|
+
if not credential or credential.api_key is None:
|
|
205
|
+
# Get workload access token for the agent
|
|
206
|
+
workload_token = await get_workload_token(
|
|
207
|
+
tool_context=tool_context,
|
|
208
|
+
identity_client=self._identity_client,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Fetch API key from identity service
|
|
212
|
+
api_key = self._identity_client.get_api_key(
|
|
213
|
+
provider_name=self._provider_name,
|
|
214
|
+
agent_identity_token=workload_token,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Create and cache the credential
|
|
218
|
+
credential = AuthCredential(
|
|
219
|
+
auth_type=AuthCredentialTypes.API_KEY,
|
|
220
|
+
api_key=api_key,
|
|
221
|
+
)
|
|
222
|
+
tool_context._invocation_context.session.state[credential_key] = credential
|
|
223
|
+
|
|
224
|
+
return credential
|
|
225
|
+
|
|
226
|
+
async def _execute_with_credential(
|
|
227
|
+
self,
|
|
228
|
+
*,
|
|
229
|
+
args: dict[str, Any],
|
|
230
|
+
tool_context: ToolContext | ReadonlyContext,
|
|
231
|
+
credential: AuthCredential,
|
|
232
|
+
) -> Any:
|
|
233
|
+
"""Default implementation - should be overridden by concrete tool classes.
|
|
234
|
+
|
|
235
|
+
This provides a base implementation to satisfy the abstract method requirement.
|
|
236
|
+
Concrete tool classes should override this method with their specific logic.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
args: Arguments to pass to the tool.
|
|
240
|
+
tool_context: The tool context.
|
|
241
|
+
credential: The authentication credential.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The result from the tool execution.
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
NotImplementedError: Always, as this should be overridden.
|
|
248
|
+
"""
|
|
249
|
+
raise NotImplementedError(
|
|
250
|
+
f"{self.__class__.__name__} must override _execute_with_credential method"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class WorkloadAuthMixin(BaseAuthMixin):
|
|
255
|
+
"""Mixin for Workload Access Token authentication via Identity Service.
|
|
256
|
+
|
|
257
|
+
This mixin handles:
|
|
258
|
+
1. Retrieving workload access tokens for the agent
|
|
259
|
+
2. Fetching API keys from the identity service
|
|
260
|
+
3. Caching API keys in session state
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
async def _get_credential(
|
|
264
|
+
self, *, tool_context: ToolContext | ReadonlyContext
|
|
265
|
+
) -> AuthCredential:
|
|
266
|
+
"""Get or create Workload Access Token credential.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
tool_context: The tool context for accessing session state.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The Workload Access Token credential.
|
|
273
|
+
"""
|
|
274
|
+
# Build cache key for the Workload Access Token
|
|
275
|
+
credential_key = (
|
|
276
|
+
f"workload_access_token:{tool_context.agent_name}:"
|
|
277
|
+
f"{tool_context._invocation_context.user_id}:{self._provider_name}"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Try to get cached Workload Access Token
|
|
281
|
+
credential: AuthCredential = tool_context._invocation_context.session.state.get(
|
|
282
|
+
credential_key
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Fetch Workload Access Token if not cached
|
|
286
|
+
if not credential or credential.api_key is None:
|
|
287
|
+
# Get workload access token for the agent
|
|
288
|
+
workload_access_token = await get_workload_token(
|
|
289
|
+
tool_context=tool_context,
|
|
290
|
+
identity_client=self._identity_client,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Create and cache the credential
|
|
294
|
+
credential = AuthCredential(
|
|
295
|
+
auth_type=AuthCredentialTypes.OAUTH2,
|
|
296
|
+
oauth2=OAuth2Auth(access_token=workload_access_token),
|
|
297
|
+
)
|
|
298
|
+
tool_context._invocation_context.session.state[credential_key] = credential
|
|
299
|
+
|
|
300
|
+
return credential
|
|
301
|
+
|
|
302
|
+
async def _execute_with_credential(
|
|
303
|
+
self,
|
|
304
|
+
*,
|
|
305
|
+
args: dict[str, Any],
|
|
306
|
+
tool_context: ToolContext | ReadonlyContext,
|
|
307
|
+
credential: AuthCredential,
|
|
308
|
+
) -> Any:
|
|
309
|
+
"""Default implementation - should be overridden by concrete tool classes.
|
|
310
|
+
|
|
311
|
+
This provides a base implementation to satisfy the abstract method requirement.
|
|
312
|
+
Concrete tool classes should override this method with their specific logic.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
args: Arguments to pass to the tool.
|
|
316
|
+
tool_context: The tool context.
|
|
317
|
+
credential: The authentication credential.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
The result from the tool execution.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
NotImplementedError: Always, as this should be overridden.
|
|
324
|
+
"""
|
|
325
|
+
raise NotImplementedError(
|
|
326
|
+
f"{self.__class__.__name__} must override _execute_with_credential method"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class OAuth2AuthMixin(BaseAuthMixin):
|
|
331
|
+
"""Mixin for OAuth2 authentication via Identity Service.
|
|
332
|
+
|
|
333
|
+
This mixin handles:
|
|
334
|
+
1. Retrieving workload access tokens for the agent
|
|
335
|
+
2. Requesting OAuth2 tokens from the identity service
|
|
336
|
+
3. Handling user authorization flows when needed
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
def __init__(
|
|
340
|
+
self,
|
|
341
|
+
*,
|
|
342
|
+
scopes: Optional[List[str]] = None,
|
|
343
|
+
auth_flow: Optional[Literal["M2M", "USER_FEDERATION"]] = None,
|
|
344
|
+
callback_url: Optional[str] = None,
|
|
345
|
+
force_authentication: bool = False,
|
|
346
|
+
response_for_auth_required: Optional[str] = None,
|
|
347
|
+
on_auth_url: Optional[Callable[[str], Any]] = None,
|
|
348
|
+
# Currently we only use auth_uri to initialize poller, may extend to support other fields like exchanged_auth_credential.
|
|
349
|
+
oauth2_auth_poller: Optional[Callable[[Any], OAuth2AuthPoller]] = None,
|
|
350
|
+
**kwargs,
|
|
351
|
+
):
|
|
352
|
+
"""Initialize the OAuth2 authentication mixin.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
scopes: Optional list of OAuth2 scopes to request. If not provided,
|
|
356
|
+
the control plane will use the default configured scopes.
|
|
357
|
+
auth_flow: Optional authentication flow type - "M2M" for machine-to-machine or
|
|
358
|
+
"USER_FEDERATION" for user-delegated access. If not provided,
|
|
359
|
+
the control plane will use the default configured flow.
|
|
360
|
+
callback_url: OAuth2 redirect URL (must be pre-registered).
|
|
361
|
+
force_authentication: If True, forces re-authentication even if cached token exists.
|
|
362
|
+
response_for_auth_required: Custom response to return when user authorization is needed.
|
|
363
|
+
If None, returns "Pending User Authorization.".
|
|
364
|
+
**kwargs: Additional arguments passed to parent classes.
|
|
365
|
+
"""
|
|
366
|
+
super().__init__(**kwargs)
|
|
367
|
+
self._scopes = scopes
|
|
368
|
+
self._auth_flow = auth_flow
|
|
369
|
+
self._callback_url = callback_url
|
|
370
|
+
self._force_authentication = force_authentication
|
|
371
|
+
self._response_for_auth_required = response_for_auth_required
|
|
372
|
+
self._on_auth_url = on_auth_url
|
|
373
|
+
self._oauth2_auth_poller = oauth2_auth_poller
|
|
374
|
+
|
|
375
|
+
async def _get_oauth2_token_or_auth_url(
|
|
376
|
+
self, *, tool_context: ToolContext | ReadonlyContext
|
|
377
|
+
) -> OAuth2TokenResponse:
|
|
378
|
+
"""Retrieve OAuth2 token or authorization URL from identity service.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
tool_context: The tool context for accessing session state and user info.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Dictionary with either a token or authorization URL.
|
|
385
|
+
"""
|
|
386
|
+
# Get workload access token for the agent
|
|
387
|
+
workload_token = await get_workload_token(
|
|
388
|
+
tool_context=tool_context,
|
|
389
|
+
identity_client=self._identity_client,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Request OAuth2 token or auth URL
|
|
393
|
+
return self._identity_client.get_oauth2_token_or_auth_url(
|
|
394
|
+
provider_name=self._provider_name,
|
|
395
|
+
agent_identity_token=workload_token,
|
|
396
|
+
auth_flow=self._auth_flow,
|
|
397
|
+
scopes=self._scopes,
|
|
398
|
+
callback_url=self._callback_url,
|
|
399
|
+
force_authentication=self._force_authentication,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
async def _get_credential(
|
|
403
|
+
self, *, tool_context: ToolContext | ReadonlyContext
|
|
404
|
+
) -> AuthCredential:
|
|
405
|
+
"""Get or create OAuth2 credential.
|
|
406
|
+
|
|
407
|
+
This method handles the OAuth2 flow and may raise an exception
|
|
408
|
+
if user authorization is required.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
tool_context: The tool context for accessing session state.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
The OAuth2 credential.
|
|
415
|
+
|
|
416
|
+
Raises:
|
|
417
|
+
AuthRequiredException: If user authorization is required.
|
|
418
|
+
"""
|
|
419
|
+
auth_config = AuthConfig(
|
|
420
|
+
auth_scheme=oauth2_scheme,
|
|
421
|
+
credential_key=f"oauth_access_token:{tool_context.agent_name}:{tool_context._invocation_context.user_id}:{self._provider_name}",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Check if we already have a credential from previous auth
|
|
425
|
+
if credential := AuthHandler(auth_config).get_auth_response(tool_context.state):
|
|
426
|
+
# Use existing credential
|
|
427
|
+
return credential
|
|
428
|
+
|
|
429
|
+
# No credential yet - request token or auth URL
|
|
430
|
+
response = await self._get_oauth2_token_or_auth_url(tool_context=tool_context)
|
|
431
|
+
|
|
432
|
+
if response.response_type == "auth_url":
|
|
433
|
+
# Need user authorization
|
|
434
|
+
auth_uri = urllib.parse.unquote(response.authorization_url)
|
|
435
|
+
if isinstance(tool_context, ToolContext):
|
|
436
|
+
# For ToolContext, use the standard request_credential flow
|
|
437
|
+
auth_config.raw_auth_credential = AuthCredential(
|
|
438
|
+
auth_type=AuthCredentialTypes.OAUTH2,
|
|
439
|
+
oauth2=OAuth2Auth(auth_uri=auth_uri),
|
|
440
|
+
resource_ref=response.resource_ref,
|
|
441
|
+
)
|
|
442
|
+
tool_context.request_credential(auth_config=auth_config) # type: ignore
|
|
443
|
+
|
|
444
|
+
# Raise a special exception to indicate auth is required
|
|
445
|
+
raise AuthRequiredException(
|
|
446
|
+
self._response_for_auth_required or "Pending User Authorization."
|
|
447
|
+
)
|
|
448
|
+
else:
|
|
449
|
+
# For ReadonlyContext (e.g., in get_tools), handle OAuth2 flow directly
|
|
450
|
+
return await self._handle_oauth2_flow_for_readonly_context(
|
|
451
|
+
auth_uri=auth_uri,
|
|
452
|
+
resource_ref=response.resource_ref,
|
|
453
|
+
readonly_context=tool_context,
|
|
454
|
+
)
|
|
455
|
+
else:
|
|
456
|
+
# Got token directly - create credential
|
|
457
|
+
return AuthCredential(
|
|
458
|
+
auth_type=AuthCredentialTypes.OAUTH2,
|
|
459
|
+
oauth2=OAuth2Auth(access_token=response.access_token),
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
async def _handle_oauth2_flow_for_readonly_context(
|
|
463
|
+
self,
|
|
464
|
+
*,
|
|
465
|
+
auth_uri: str,
|
|
466
|
+
resource_ref: Optional[str],
|
|
467
|
+
readonly_context: ReadonlyContext,
|
|
468
|
+
) -> AuthCredential:
|
|
469
|
+
"""Handle OAuth2 flow for ReadonlyContext (e.g., during get_tools).
|
|
470
|
+
|
|
471
|
+
This method implements a direct OAuth2 flow similar to auth_processor.py
|
|
472
|
+
but adapted for ReadonlyContext scenarios.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
auth_uri: The OAuth2 authorization URI
|
|
476
|
+
resource_ref: Resource reference for the OAuth2 request
|
|
477
|
+
tool_context: The readonly context
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
AuthCredential with OAuth2 access token
|
|
481
|
+
"""
|
|
482
|
+
import json
|
|
483
|
+
|
|
484
|
+
# Parse resource_ref if available
|
|
485
|
+
request_dict = json.loads(resource_ref) if resource_ref else {}
|
|
486
|
+
|
|
487
|
+
# Use custom on_auth_url callback if provided
|
|
488
|
+
if on_auth_url := self._on_auth_url:
|
|
489
|
+
if asyncio.iscoroutinefunction(on_auth_url):
|
|
490
|
+
await on_auth_url(auth_uri)
|
|
491
|
+
else:
|
|
492
|
+
on_auth_url(auth_uri)
|
|
493
|
+
else:
|
|
494
|
+
logger.info(f"Authorization required: {auth_uri}")
|
|
495
|
+
|
|
496
|
+
# Use custom oauth2_auth_poller if provided
|
|
497
|
+
if oauth2_auth_poller := self._oauth2_auth_poller:
|
|
498
|
+
active_poller = oauth2_auth_poller(auth_uri, request_dict)
|
|
499
|
+
else:
|
|
500
|
+
# Use default poller
|
|
501
|
+
active_poller = self._create_default_oauth2_poller(auth_uri, request_dict)
|
|
502
|
+
|
|
503
|
+
# Poll for the OAuth2 auth
|
|
504
|
+
updated_oauth2_auth = await active_poller.poll_for_auth()
|
|
505
|
+
|
|
506
|
+
if not updated_oauth2_auth or not updated_oauth2_auth.access_token:
|
|
507
|
+
raise AuthRequiredException("Failed to obtain OAuth2 access token")
|
|
508
|
+
|
|
509
|
+
return AuthCredential(
|
|
510
|
+
auth_type=AuthCredentialTypes.OAUTH2,
|
|
511
|
+
oauth2=updated_oauth2_auth,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
def _create_default_oauth2_poller(self, auth_uri: str, request_dict: dict):
|
|
515
|
+
"""Create a default OAuth2 poller for ReadonlyContext scenarios."""
|
|
516
|
+
from veadk.integrations.ve_identity.auth_processor import (
|
|
517
|
+
_DefaultOauth2AuthPoller,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
async def async_token_fetcher():
|
|
521
|
+
response = self._identity_client.get_oauth2_token_or_auth_url(
|
|
522
|
+
**request_dict
|
|
523
|
+
)
|
|
524
|
+
return (
|
|
525
|
+
OAuth2Auth(access_token=response.access_token)
|
|
526
|
+
if response.access_token and response.access_token.strip()
|
|
527
|
+
else None
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
return _DefaultOauth2AuthPoller(auth_uri, async_token_fetcher)
|
|
531
|
+
|
|
532
|
+
async def _execute_with_credential(
|
|
533
|
+
self,
|
|
534
|
+
*,
|
|
535
|
+
args: dict[str, Any],
|
|
536
|
+
tool_context: ToolContext | ReadonlyContext,
|
|
537
|
+
credential: AuthCredential,
|
|
538
|
+
) -> Any:
|
|
539
|
+
"""Default implementation - should be overridden by concrete tool classes.
|
|
540
|
+
|
|
541
|
+
This provides a base implementation to satisfy the abstract method requirement.
|
|
542
|
+
Concrete tool classes should override this method with their specific logic.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
args: Arguments to pass to the tool.
|
|
546
|
+
tool_context: The tool context.
|
|
547
|
+
credential: The authentication credential.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
The result from the tool execution.
|
|
551
|
+
|
|
552
|
+
Raises:
|
|
553
|
+
NotImplementedError: Always, as this should be overridden.
|
|
554
|
+
"""
|
|
555
|
+
raise NotImplementedError(
|
|
556
|
+
f"{self.__class__.__name__} must override _execute_with_credential method"
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class VeIdentityAuthMixin(BaseAuthMixin):
|
|
561
|
+
"""Unified mixin that supports both API Key and OAuth2 authentication based on configuration.
|
|
562
|
+
|
|
563
|
+
This mixin uses composition to delegate to specific authentication implementations
|
|
564
|
+
while providing a unified interface for tools.
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
def __init__(self, *, auth_config: VeIdentityAuthConfig, **kwargs):
|
|
568
|
+
"""Initialize the unified authentication mixin.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
auth_config: Authentication configuration (ApiKeyAuthConfig, WorkloadAuthConfig, or OAuth2AuthConfig).
|
|
572
|
+
**kwargs: Additional arguments passed to parent classes.
|
|
573
|
+
"""
|
|
574
|
+
# Initialize base class with common parameters
|
|
575
|
+
super().__init__(
|
|
576
|
+
provider_name=auth_config.provider_name,
|
|
577
|
+
identity_client=auth_config.identity_client,
|
|
578
|
+
region=auth_config.region,
|
|
579
|
+
**kwargs,
|
|
580
|
+
)
|
|
581
|
+
self._auth_config = auth_config
|
|
582
|
+
|
|
583
|
+
# Create the appropriate authentication delegate based on config type
|
|
584
|
+
self._auth_delegate = self._create_auth_delegate(**kwargs)
|
|
585
|
+
|
|
586
|
+
def _create_auth_delegate(self, **kwargs):
|
|
587
|
+
"""Create the appropriate authentication delegate based on config type."""
|
|
588
|
+
if isinstance(self._auth_config, ApiKeyAuthConfig):
|
|
589
|
+
return ApiKeyAuthMixin(
|
|
590
|
+
provider_name=self._auth_config.provider_name,
|
|
591
|
+
identity_client=self._auth_config.identity_client,
|
|
592
|
+
region=self._auth_config.region,
|
|
593
|
+
**kwargs,
|
|
594
|
+
)
|
|
595
|
+
elif isinstance(self._auth_config, WorkloadAuthConfig):
|
|
596
|
+
return WorkloadAuthMixin(
|
|
597
|
+
provider_name=self._auth_config.provider_name,
|
|
598
|
+
identity_client=self._auth_config.identity_client,
|
|
599
|
+
region=self._auth_config.region,
|
|
600
|
+
**kwargs,
|
|
601
|
+
)
|
|
602
|
+
elif isinstance(self._auth_config, OAuth2AuthConfig):
|
|
603
|
+
return OAuth2AuthMixin(
|
|
604
|
+
provider_name=self._auth_config.provider_name,
|
|
605
|
+
scopes=self._auth_config.scopes,
|
|
606
|
+
auth_flow=self._auth_config.auth_flow,
|
|
607
|
+
callback_url=self._auth_config.callback_url,
|
|
608
|
+
force_authentication=self._auth_config.force_authentication,
|
|
609
|
+
response_for_auth_required=self._auth_config.response_for_auth_required,
|
|
610
|
+
on_auth_url=self._auth_config.on_auth_url,
|
|
611
|
+
oauth2_auth_poller=self._auth_config.oauth2_auth_poller,
|
|
612
|
+
identity_client=self._auth_config.identity_client,
|
|
613
|
+
region=self._auth_config.region,
|
|
614
|
+
**kwargs,
|
|
615
|
+
)
|
|
616
|
+
else:
|
|
617
|
+
raise ValueError(f"Unsupported auth config type: {type(self._auth_config)}")
|
|
618
|
+
|
|
619
|
+
async def _get_credential(
|
|
620
|
+
self, *, tool_context: ToolContext | ReadonlyContext
|
|
621
|
+
) -> AuthCredential:
|
|
622
|
+
"""Get or create authentication credential using the configured auth type."""
|
|
623
|
+
return await self._auth_delegate._get_credential(tool_context=tool_context)
|
|
624
|
+
|
|
625
|
+
async def _execute_with_credential(
|
|
626
|
+
self,
|
|
627
|
+
*,
|
|
628
|
+
args: dict[str, Any],
|
|
629
|
+
tool_context: ToolContext | ReadonlyContext,
|
|
630
|
+
credential: AuthCredential,
|
|
631
|
+
) -> Any:
|
|
632
|
+
"""Default implementation - should be overridden by concrete tool classes.
|
|
633
|
+
|
|
634
|
+
This provides a base implementation to satisfy the abstract method requirement.
|
|
635
|
+
Concrete tool classes should override this method with their specific logic.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
args: Arguments to pass to the tool.
|
|
639
|
+
tool_context: The tool context.
|
|
640
|
+
credential: The authentication credential.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
The result from the tool execution.
|
|
644
|
+
|
|
645
|
+
Raises:
|
|
646
|
+
NotImplementedError: Always, as this should be overridden.
|
|
647
|
+
"""
|
|
648
|
+
raise NotImplementedError(
|
|
649
|
+
f"{self.__class__.__name__} must override _execute_with_credential method"
|
|
650
|
+
)
|