glaip-sdk 0.6.2__py3-none-any.whl → 0.6.4__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.
- glaip_sdk/agents/base.py +54 -8
- glaip_sdk/cli/account_store.py +36 -18
- glaip_sdk/cli/auth.py +1 -1
- glaip_sdk/cli/commands/accounts.py +2 -2
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/slash/accounts_controller.py +308 -25
- glaip_sdk/cli/slash/accounts_shared.py +57 -1
- glaip_sdk/cli/slash/session.py +109 -24
- glaip_sdk/cli/slash/tui/accounts.tcss +33 -1
- glaip_sdk/cli/slash/tui/accounts_app.py +525 -32
- glaip_sdk/cli/slash/tui/remote_runs_app.py +3 -3
- glaip_sdk/cli/utils.py +241 -1732
- glaip_sdk/registry/mcp.py +8 -6
- glaip_sdk/utils/validation.py +3 -3
- {glaip_sdk-0.6.2.dist-info → glaip_sdk-0.6.4.dist-info}/METADATA +1 -1
- {glaip_sdk-0.6.2.dist-info → glaip_sdk-0.6.4.dist-info}/RECORD +22 -17
- {glaip_sdk-0.6.2.dist-info → glaip_sdk-0.6.4.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.2.dist-info → glaip_sdk-0.6.4.dist-info}/entry_points.txt +0 -0
glaip_sdk/agents/base.py
CHANGED
|
@@ -51,10 +51,12 @@ from typing import TYPE_CHECKING, Any
|
|
|
51
51
|
|
|
52
52
|
from glaip_sdk.registry import get_agent_registry, get_mcp_registry, get_tool_registry
|
|
53
53
|
from glaip_sdk.utils.discovery import find_agent
|
|
54
|
+
from glaip_sdk.utils.resource_refs import is_uuid
|
|
54
55
|
from glaip_sdk.utils.runtime_config import normalize_runtime_config_keys
|
|
55
56
|
|
|
56
57
|
if TYPE_CHECKING:
|
|
57
58
|
from glaip_sdk.models import AgentResponse
|
|
59
|
+
from glaip_sdk.registry import AgentRegistry, MCPRegistry, ToolRegistry
|
|
58
60
|
|
|
59
61
|
logger = logging.getLogger(__name__)
|
|
60
62
|
|
|
@@ -519,7 +521,7 @@ class Agent:
|
|
|
519
521
|
|
|
520
522
|
return self
|
|
521
523
|
|
|
522
|
-
def _build_config(self, tool_registry:
|
|
524
|
+
def _build_config(self, tool_registry: ToolRegistry, mcp_registry: MCPRegistry) -> dict[str, Any]:
|
|
523
525
|
"""Build the base configuration dictionary.
|
|
524
526
|
|
|
525
527
|
Args:
|
|
@@ -558,9 +560,9 @@ class Agent:
|
|
|
558
560
|
if self.mcps:
|
|
559
561
|
config["mcps"] = self._resolve_mcps(mcp_registry)
|
|
560
562
|
|
|
561
|
-
# Handle mcp_configs
|
|
563
|
+
# Handle mcp_configs - normalize keys to MCP IDs
|
|
562
564
|
if self.mcp_configs:
|
|
563
|
-
config["mcp_configs"] = self.
|
|
565
|
+
config["mcp_configs"] = self._resolve_mcp_configs(mcp_registry)
|
|
564
566
|
|
|
565
567
|
# Handle a2a_profile
|
|
566
568
|
if self.a2a_profile:
|
|
@@ -568,7 +570,7 @@ class Agent:
|
|
|
568
570
|
|
|
569
571
|
return config
|
|
570
572
|
|
|
571
|
-
def _resolve_mcps(self, registry:
|
|
573
|
+
def _resolve_mcps(self, registry: MCPRegistry) -> list[str]:
|
|
572
574
|
"""Resolve MCP references to IDs using MCPRegistry.
|
|
573
575
|
|
|
574
576
|
Uses the global MCPRegistry to cache MCP objects across deployments.
|
|
@@ -586,7 +588,7 @@ class Agent:
|
|
|
586
588
|
|
|
587
589
|
return [registry.resolve(mcp_ref).id for mcp_ref in self.mcps]
|
|
588
590
|
|
|
589
|
-
def _resolve_tools(self, registry:
|
|
591
|
+
def _resolve_tools(self, registry: ToolRegistry) -> list[str]:
|
|
590
592
|
"""Resolve tool references to IDs using ToolRegistry.
|
|
591
593
|
|
|
592
594
|
Uses the global ToolRegistry to cache Tool objects across deployments.
|
|
@@ -605,7 +607,7 @@ class Agent:
|
|
|
605
607
|
# Resolve each tool reference to a Tool object, extract ID
|
|
606
608
|
return [registry.resolve(tool_ref).id for tool_ref in self.tools]
|
|
607
609
|
|
|
608
|
-
def _resolve_tool_configs(self, registry:
|
|
610
|
+
def _resolve_tool_configs(self, registry: ToolRegistry) -> dict[str, Any]:
|
|
609
611
|
"""Resolve tool_configs keys from tool names/classes to tool IDs.
|
|
610
612
|
|
|
611
613
|
Allows tool_configs to be defined with tool names, class names, or
|
|
@@ -639,7 +641,7 @@ class Agent:
|
|
|
639
641
|
|
|
640
642
|
for key, config in self.tool_configs.items():
|
|
641
643
|
# If key is already a UUID-like string, pass through
|
|
642
|
-
if isinstance(key, str) and
|
|
644
|
+
if isinstance(key, str) and is_uuid(key):
|
|
643
645
|
resolved[key] = config
|
|
644
646
|
continue
|
|
645
647
|
|
|
@@ -652,7 +654,51 @@ class Agent:
|
|
|
652
654
|
|
|
653
655
|
return resolved
|
|
654
656
|
|
|
655
|
-
def
|
|
657
|
+
def _resolve_mcp_configs(self, registry: MCPRegistry) -> dict[str, Any]:
|
|
658
|
+
"""Resolve mcp_configs keys from MCP names/objects to MCP IDs.
|
|
659
|
+
|
|
660
|
+
Allows mcp_configs to be defined with MCP names, MCP objects, or UUIDs
|
|
661
|
+
as keys. Keys are resolved to MCP IDs using the MCPRegistry.
|
|
662
|
+
|
|
663
|
+
Supported key formats:
|
|
664
|
+
- MCP object (with id): uses id directly
|
|
665
|
+
- MCP name string: resolved via registry to ID
|
|
666
|
+
- MCP ID (UUID string): passed through unchanged
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
registry: The MCP registry.
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
Dict with resolved MCP IDs as keys and configs as values.
|
|
673
|
+
"""
|
|
674
|
+
if not self.mcp_configs:
|
|
675
|
+
return {}
|
|
676
|
+
|
|
677
|
+
resolved: dict[str, Any] = {}
|
|
678
|
+
|
|
679
|
+
for key, config in self.mcp_configs.items():
|
|
680
|
+
try:
|
|
681
|
+
# If key is already a UUID-like string, pass through
|
|
682
|
+
if isinstance(key, str) and is_uuid(key):
|
|
683
|
+
resolved_id = key
|
|
684
|
+
else:
|
|
685
|
+
mcp = registry.resolve(key)
|
|
686
|
+
resolved_id = mcp.id
|
|
687
|
+
|
|
688
|
+
if resolved_id in resolved:
|
|
689
|
+
raise ValueError(
|
|
690
|
+
f"Duplicate mcp_configs entries resolve to the same MCP id '{resolved_id}' (key={key!r})"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
resolved[resolved_id] = config
|
|
694
|
+
except (ValueError, KeyError) as exc:
|
|
695
|
+
raise ValueError(
|
|
696
|
+
f"Failed to resolve mcp config key {key!r} (type={type(key).__name__}): {exc}"
|
|
697
|
+
) from exc
|
|
698
|
+
|
|
699
|
+
return resolved
|
|
700
|
+
|
|
701
|
+
def _resolve_agents(self, registry: AgentRegistry) -> list:
|
|
656
702
|
"""Resolve sub-agent references using AgentRegistry.
|
|
657
703
|
|
|
658
704
|
Uses the global AgentRegistry to cache Agent objects across deployments.
|
glaip_sdk/cli/account_store.py
CHANGED
|
@@ -217,13 +217,14 @@ class AccountStore:
|
|
|
217
217
|
api_key: API key or None.
|
|
218
218
|
|
|
219
219
|
Returns:
|
|
220
|
-
Dictionary with "default" account if credentials exist, empty dict otherwise.
|
|
220
|
+
Dictionary with "default" account if both credentials exist and are non-empty, empty dict otherwise.
|
|
221
221
|
"""
|
|
222
222
|
accounts = {}
|
|
223
|
-
if
|
|
223
|
+
# Only create default account if both URL and key are present and non-empty
|
|
224
|
+
if api_url and api_key and api_url.strip() and api_key.strip():
|
|
224
225
|
accounts["default"] = {
|
|
225
|
-
"api_url": api_url
|
|
226
|
-
"api_key": api_key
|
|
226
|
+
"api_url": api_url.strip(),
|
|
227
|
+
"api_key": api_key.strip(),
|
|
227
228
|
}
|
|
228
229
|
return accounts
|
|
229
230
|
|
|
@@ -257,15 +258,30 @@ class AccountStore:
|
|
|
257
258
|
"accounts": {},
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
# Preserve existing accounts if they exist (shouldn't happen in true migration, but defensive)
|
|
262
|
+
existing_accounts = config.get("accounts", {})
|
|
263
|
+
if existing_accounts:
|
|
264
|
+
migrated["accounts"] = existing_accounts.copy()
|
|
265
|
+
existing_active = config.get("active_account")
|
|
266
|
+
if existing_active and existing_active in existing_accounts:
|
|
267
|
+
migrated["active_account"] = existing_active
|
|
268
|
+
elif "default" in existing_accounts:
|
|
269
|
+
migrated["active_account"] = "default"
|
|
270
|
+
else:
|
|
271
|
+
migrated["active_account"] = sorted(existing_accounts.keys())[0]
|
|
272
|
+
else:
|
|
273
|
+
# Extract legacy api_url and api_key only if no accounts exist
|
|
274
|
+
api_url = config.get("api_url")
|
|
275
|
+
api_key = config.get("api_key")
|
|
263
276
|
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
# Check for auth.json from secure login MVP (only during migration)
|
|
278
|
+
api_url, api_key = self._load_auth_json_credentials(api_url, api_key)
|
|
266
279
|
|
|
267
|
-
|
|
268
|
-
|
|
280
|
+
# Create default account if we have valid credentials
|
|
281
|
+
migrated["accounts"] = self._create_default_account(api_url, api_key)
|
|
282
|
+
# Only set active_account to default if we actually created a default account
|
|
283
|
+
if not migrated["accounts"]:
|
|
284
|
+
migrated.pop("active_account", None)
|
|
269
285
|
|
|
270
286
|
# Preserve other top-level keys for backward compatibility
|
|
271
287
|
migrated.update(self._preserve_legacy_keys(config))
|
|
@@ -423,16 +439,18 @@ class AccountStore:
|
|
|
423
439
|
|
|
424
440
|
del accounts[name]
|
|
425
441
|
|
|
426
|
-
# If we removed the active account, switch to another
|
|
442
|
+
# If we removed the active account, switch to another account
|
|
427
443
|
active_account = config.get("active_account")
|
|
428
444
|
if active_account == name:
|
|
429
|
-
#
|
|
430
|
-
|
|
431
|
-
if "default" in remaining_names:
|
|
445
|
+
# Prefer "default" if it exists, otherwise use first alphabetical account
|
|
446
|
+
if "default" in accounts:
|
|
432
447
|
config["active_account"] = "default"
|
|
433
|
-
elif
|
|
434
|
-
|
|
435
|
-
|
|
448
|
+
elif accounts:
|
|
449
|
+
# Sort accounts alphabetically and pick the first one
|
|
450
|
+
sorted_names = sorted(accounts.keys())
|
|
451
|
+
config["active_account"] = sorted_names[0]
|
|
452
|
+
else:
|
|
453
|
+
# No accounts remaining (shouldn't happen due to check above)
|
|
436
454
|
config.pop("active_account", None)
|
|
437
455
|
|
|
438
456
|
self._save_config(config)
|
glaip_sdk/cli/auth.py
CHANGED
|
@@ -6,7 +6,6 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
import getpass
|
|
8
8
|
import json
|
|
9
|
-
import os
|
|
10
9
|
import sys
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
|
|
@@ -33,6 +32,7 @@ from glaip_sdk.cli.account_store import (
|
|
|
33
32
|
from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
|
|
34
33
|
from glaip_sdk.cli.hints import format_command_hint
|
|
35
34
|
from glaip_sdk.cli.masking import mask_api_key_display
|
|
35
|
+
from glaip_sdk.cli.slash.accounts_shared import env_credentials_present
|
|
36
36
|
from glaip_sdk.cli.utils import command_hint
|
|
37
37
|
from glaip_sdk.icons import ICON_TOOL
|
|
38
38
|
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
@@ -231,7 +231,7 @@ def show_account(name: str, output_json: bool) -> None:
|
|
|
231
231
|
masked_key = _mask_api_key(api_key or "")
|
|
232
232
|
active_account = store.get_active_account()
|
|
233
233
|
is_active = active_account == name
|
|
234
|
-
env_lock =
|
|
234
|
+
env_lock = env_credentials_present(partial=True)
|
|
235
235
|
config_path_raw = str(store.config_file)
|
|
236
236
|
config_path_display = _format_config_path(config_path_raw)
|
|
237
237
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""CLI core modules for glaip-sdk.
|
|
2
|
+
|
|
3
|
+
This package contains focused modules extracted from the monolithic cli/utils.py:
|
|
4
|
+
- context: Click context helpers, config loading, credential resolution
|
|
5
|
+
- prompting: prompt_toolkit + questionary wrappers, validators
|
|
6
|
+
- rendering: Rich console helpers, viewer launchers, renderer builders
|
|
7
|
+
- output: Table/console output utilities, list rendering
|
|
8
|
+
|
|
9
|
+
Authors:
|
|
10
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
11
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
12
|
+
""" # pylint: disable=duplicate-code
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
# Re-export all public APIs from submodules for convenience
|
|
17
|
+
from glaip_sdk.cli.core.context import (
|
|
18
|
+
bind_slash_session_context,
|
|
19
|
+
get_client,
|
|
20
|
+
handle_best_effort_check,
|
|
21
|
+
restore_slash_session_context,
|
|
22
|
+
)
|
|
23
|
+
from glaip_sdk.cli.core.output import (
|
|
24
|
+
coerce_to_row,
|
|
25
|
+
detect_export_format,
|
|
26
|
+
fetch_resource_for_export,
|
|
27
|
+
format_datetime_fields,
|
|
28
|
+
format_size,
|
|
29
|
+
handle_resource_export,
|
|
30
|
+
output_list,
|
|
31
|
+
output_result,
|
|
32
|
+
parse_json_line,
|
|
33
|
+
resolve_resource,
|
|
34
|
+
handle_ambiguous_resource,
|
|
35
|
+
sdk_version,
|
|
36
|
+
)
|
|
37
|
+
from glaip_sdk.cli.core.prompting import (
|
|
38
|
+
_fuzzy_pick_for_resources,
|
|
39
|
+
prompt_export_choice_questionary,
|
|
40
|
+
questionary_safe_ask,
|
|
41
|
+
)
|
|
42
|
+
from glaip_sdk.cli.core.rendering import (
|
|
43
|
+
build_renderer,
|
|
44
|
+
spinner_context,
|
|
45
|
+
stop_spinner,
|
|
46
|
+
update_spinner,
|
|
47
|
+
with_client_and_spinner,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# Context
|
|
52
|
+
"bind_slash_session_context",
|
|
53
|
+
"get_client",
|
|
54
|
+
"handle_best_effort_check",
|
|
55
|
+
"restore_slash_session_context",
|
|
56
|
+
# Prompting
|
|
57
|
+
"_fuzzy_pick_for_resources",
|
|
58
|
+
"prompt_export_choice_questionary",
|
|
59
|
+
"questionary_safe_ask",
|
|
60
|
+
# Rendering
|
|
61
|
+
"build_renderer",
|
|
62
|
+
"spinner_context",
|
|
63
|
+
"stop_spinner",
|
|
64
|
+
"update_spinner",
|
|
65
|
+
"with_client_and_spinner",
|
|
66
|
+
# Output
|
|
67
|
+
"coerce_to_row",
|
|
68
|
+
"detect_export_format",
|
|
69
|
+
"fetch_resource_for_export",
|
|
70
|
+
"format_datetime_fields",
|
|
71
|
+
"format_size",
|
|
72
|
+
"handle_resource_export",
|
|
73
|
+
"output_list",
|
|
74
|
+
"output_result",
|
|
75
|
+
"parse_json_line",
|
|
76
|
+
"resolve_resource",
|
|
77
|
+
"handle_ambiguous_resource",
|
|
78
|
+
"sdk_version",
|
|
79
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""CLI context helpers, config loading, and credential resolution.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import os
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from glaip_sdk.cli.config import load_config
|
|
19
|
+
from glaip_sdk.cli.hints import command_hint
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING: # pragma: no cover - import-only during type checking
|
|
22
|
+
from glaip_sdk import Client
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def bind_slash_session_context(ctx: Any, session: Any) -> Any:
|
|
27
|
+
"""Temporarily attach a slash session to the Click context.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
ctx: Click context object.
|
|
31
|
+
session: SlashSession instance to bind.
|
|
32
|
+
|
|
33
|
+
Yields:
|
|
34
|
+
None - context manager for use in with statement.
|
|
35
|
+
"""
|
|
36
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
37
|
+
has_context = isinstance(ctx_obj, dict)
|
|
38
|
+
previous_session = ctx_obj.get("_slash_session") if has_context else None
|
|
39
|
+
if has_context:
|
|
40
|
+
ctx_obj["_slash_session"] = session
|
|
41
|
+
try:
|
|
42
|
+
yield
|
|
43
|
+
finally:
|
|
44
|
+
if has_context:
|
|
45
|
+
if previous_session is None:
|
|
46
|
+
ctx_obj.pop("_slash_session", None)
|
|
47
|
+
else:
|
|
48
|
+
ctx_obj["_slash_session"] = previous_session
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def restore_slash_session_context(ctx_obj: dict[str, Any], previous_session: Any | None) -> None:
|
|
52
|
+
"""Restore slash session context after operation.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
ctx_obj: Click context obj dictionary.
|
|
56
|
+
previous_session: Previous session to restore, or None to remove.
|
|
57
|
+
"""
|
|
58
|
+
if previous_session is None:
|
|
59
|
+
ctx_obj.pop("_slash_session", None)
|
|
60
|
+
else:
|
|
61
|
+
ctx_obj["_slash_session"] = previous_session
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def handle_best_effort_check(
|
|
65
|
+
check_func: Any,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Handle best-effort duplicate/existence checks with proper exception handling.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
check_func: Function that performs the check and raises ClickException if duplicate found.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
check_func()
|
|
74
|
+
except click.ClickException:
|
|
75
|
+
raise
|
|
76
|
+
except Exception:
|
|
77
|
+
# Non-fatal: best-effort duplicate check
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
82
|
+
"""Get configured client from context and account store (ctx > account)."""
|
|
83
|
+
# Import here to avoid circular import
|
|
84
|
+
from glaip_sdk.cli.auth import resolve_credentials # noqa: PLC0415
|
|
85
|
+
|
|
86
|
+
module = importlib.import_module("glaip_sdk")
|
|
87
|
+
client_class = cast("type[Client]", module.Client)
|
|
88
|
+
context_config_obj = getattr(ctx, "obj", None)
|
|
89
|
+
context_config = context_config_obj if isinstance(context_config_obj, Mapping) else {}
|
|
90
|
+
|
|
91
|
+
account_name = context_config.get("account_name")
|
|
92
|
+
api_url, api_key, _ = resolve_credentials(
|
|
93
|
+
account_name=account_name,
|
|
94
|
+
api_url=context_config.get("api_url"),
|
|
95
|
+
api_key=context_config.get("api_key"),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if not api_url or not api_key:
|
|
99
|
+
configure_hint = command_hint("accounts add", slash_command="login", ctx=ctx)
|
|
100
|
+
actions: list[str] = []
|
|
101
|
+
if configure_hint:
|
|
102
|
+
actions.append(f"Run `{configure_hint}` to add an account profile")
|
|
103
|
+
else:
|
|
104
|
+
actions.append("add an account with 'aip accounts add'")
|
|
105
|
+
raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
|
|
106
|
+
|
|
107
|
+
# Get timeout from context or config
|
|
108
|
+
timeout = context_config.get("timeout")
|
|
109
|
+
if timeout is None:
|
|
110
|
+
raw_timeout = os.getenv("AIP_TIMEOUT", "0") or "0"
|
|
111
|
+
try:
|
|
112
|
+
timeout = float(raw_timeout) if raw_timeout != "0" else None
|
|
113
|
+
except ValueError:
|
|
114
|
+
timeout = None
|
|
115
|
+
if timeout is None:
|
|
116
|
+
# Fallback to legacy config
|
|
117
|
+
file_config = load_config() or {}
|
|
118
|
+
timeout = file_config.get("timeout")
|
|
119
|
+
|
|
120
|
+
return client_class(
|
|
121
|
+
api_url=api_url,
|
|
122
|
+
api_key=api_key,
|
|
123
|
+
timeout=float(timeout or 30.0),
|
|
124
|
+
)
|