glaip-sdk 0.7.7__py3-none-any.whl → 0.7.9__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/cli/account_store.py +15 -0
- glaip_sdk/cli/config.py +1 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +162 -10
- glaip_sdk/cli/slash/tui/context.py +37 -12
- glaip_sdk/cli/slash/tui/theme/manager.py +28 -2
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/tools/base.py +23 -3
- {glaip_sdk-0.7.7.dist-info → glaip_sdk-0.7.9.dist-info}/METADATA +4 -4
- {glaip_sdk-0.7.7.dist-info → glaip_sdk-0.7.9.dist-info}/RECORD +12 -11
- {glaip_sdk-0.7.7.dist-info → glaip_sdk-0.7.9.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.7.7.dist-info → glaip_sdk-0.7.9.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.7.7.dist-info → glaip_sdk-0.7.9.dist-info}/top_level.txt +0 -0
glaip_sdk/cli/account_store.py
CHANGED
|
@@ -523,6 +523,21 @@ class AccountStore:
|
|
|
523
523
|
|
|
524
524
|
self._save_config(config)
|
|
525
525
|
|
|
526
|
+
def save_config_updates(self, config: dict[str, Any]) -> None:
|
|
527
|
+
"""Save config updates, preserving all existing keys.
|
|
528
|
+
|
|
529
|
+
This method allows external code to update arbitrary config keys
|
|
530
|
+
(e.g., TUI preferences) while preserving the full config structure.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
config: Complete configuration dictionary to save. This should
|
|
534
|
+
include all keys that should be preserved, not just updates.
|
|
535
|
+
|
|
536
|
+
Raises:
|
|
537
|
+
AccountStoreError: If config file cannot be written.
|
|
538
|
+
"""
|
|
539
|
+
self._save_config(config)
|
|
540
|
+
|
|
526
541
|
|
|
527
542
|
# Global instance for convenience
|
|
528
543
|
_account_store = AccountStore()
|
glaip_sdk/cli/config.py
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
Provides a minimal interactive list with the same columns/order as the Rich
|
|
4
4
|
fallback (name, API URL, masked key, status) and keyboard navigation.
|
|
5
5
|
|
|
6
|
+
Integrates with TUI foundation services:
|
|
7
|
+
- KeybindRegistry: Centralized keybind registration with scoped actions
|
|
8
|
+
- ClipboardAdapter: Cross-platform clipboard operations with OSC 52 support
|
|
9
|
+
- ToastBus: Non-blocking toast notifications for user feedback
|
|
10
|
+
|
|
6
11
|
Authors:
|
|
7
12
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
8
13
|
"""
|
|
@@ -23,10 +28,13 @@ from glaip_sdk.cli.slash.accounts_shared import (
|
|
|
23
28
|
env_credentials_present,
|
|
24
29
|
)
|
|
25
30
|
from glaip_sdk.cli.slash.tui.background_tasks import BackgroundTaskMixin
|
|
26
|
-
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
|
|
31
|
+
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardResult
|
|
27
32
|
from glaip_sdk.cli.slash.tui.context import TUIContext
|
|
33
|
+
from glaip_sdk.cli.slash.tui.keybind_registry import KeybindRegistry
|
|
28
34
|
from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
|
|
35
|
+
from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
|
|
29
36
|
from glaip_sdk.cli.slash.tui.theme.catalog import _BUILTIN_THEMES
|
|
37
|
+
from glaip_sdk.cli.slash.tui.toast import ToastBus
|
|
30
38
|
from glaip_sdk.cli.validators import validate_api_key
|
|
31
39
|
from glaip_sdk.utils.validation import validate_url
|
|
32
40
|
|
|
@@ -36,6 +44,7 @@ try: # pragma: no cover - optional dependency
|
|
|
36
44
|
from textual.binding import Binding
|
|
37
45
|
from textual.containers import Container, Horizontal, Vertical
|
|
38
46
|
from textual.screen import ModalScreen
|
|
47
|
+
from textual.suggester import SuggestFromList
|
|
39
48
|
from textual.widgets import Button, Checkbox, DataTable, Footer, Header, Input, LoadingIndicator, Static
|
|
40
49
|
except Exception: # pragma: no cover - optional dependency
|
|
41
50
|
events = None # type: ignore[assignment]
|
|
@@ -54,6 +63,7 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
54
63
|
LoadingIndicator = None # type: ignore[assignment]
|
|
55
64
|
ModalScreen = None # type: ignore[assignment]
|
|
56
65
|
Static = None # type: ignore[assignment]
|
|
66
|
+
SuggestFromList = None # type: ignore[assignment]
|
|
57
67
|
Theme = None # type: ignore[assignment]
|
|
58
68
|
|
|
59
69
|
if App is not None:
|
|
@@ -84,6 +94,30 @@ FORM_KEY_ID = "#form-key"
|
|
|
84
94
|
# CSS file name
|
|
85
95
|
CSS_FILE_NAME = "accounts.tcss"
|
|
86
96
|
|
|
97
|
+
KEYBIND_SCOPE = "accounts"
|
|
98
|
+
KEYBIND_CATEGORY = "Accounts"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class KeybindDef:
|
|
103
|
+
"""Keybind definition with action, key, and description."""
|
|
104
|
+
|
|
105
|
+
action: str
|
|
106
|
+
key: str
|
|
107
|
+
description: str
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
KEYBIND_DEFINITIONS: tuple[KeybindDef, ...] = (
|
|
111
|
+
KeybindDef("switch_row", "enter", "Switch"),
|
|
112
|
+
KeybindDef("focus_filter", "/", "Filter"),
|
|
113
|
+
KeybindDef("add_account", "a", "Add"),
|
|
114
|
+
KeybindDef("edit_account", "e", "Edit"),
|
|
115
|
+
KeybindDef("delete_account", "d", "Delete"),
|
|
116
|
+
KeybindDef("copy_account", "c", "Copy"),
|
|
117
|
+
KeybindDef("clear_or_exit", "escape", "Close"),
|
|
118
|
+
KeybindDef("app_exit", "q", "Close"),
|
|
119
|
+
)
|
|
120
|
+
|
|
87
121
|
|
|
88
122
|
@dataclass
|
|
89
123
|
class AccountsTUICallbacks:
|
|
@@ -250,6 +284,27 @@ class AccountFormModal(_AccountFormBase): # pragma: no cover - interactive
|
|
|
250
284
|
self._connection_tester = connection_tester
|
|
251
285
|
self._validate_name = validate_name
|
|
252
286
|
|
|
287
|
+
def _get_api_url_suggestions(self, _value: str) -> list[str]:
|
|
288
|
+
"""Get API URL suggestions from existing accounts.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
_value: Current input value (unused, but required by Textual's suggestor API).
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
List of unique API URLs from existing accounts.
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
store = get_account_store()
|
|
298
|
+
accounts = store.list_accounts()
|
|
299
|
+
# Extract unique API URLs, excluding the current account's URL in edit mode
|
|
300
|
+
existing_url = self._existing.get("api_url", "")
|
|
301
|
+
urls = {account.get("api_url", "") for account in accounts.values() if account.get("api_url")}
|
|
302
|
+
if existing_url in urls:
|
|
303
|
+
urls.remove(existing_url)
|
|
304
|
+
return sorted(urls)
|
|
305
|
+
except Exception: # pragma: no cover - defensive
|
|
306
|
+
return []
|
|
307
|
+
|
|
253
308
|
def compose(self) -> ComposeResult:
|
|
254
309
|
"""Render the form controls."""
|
|
255
310
|
title = "Add account" if self._mode == "add" else "Edit account"
|
|
@@ -259,7 +314,17 @@ class AccountFormModal(_AccountFormBase): # pragma: no cover - interactive
|
|
|
259
314
|
id="form-name",
|
|
260
315
|
disabled=self._mode == "edit",
|
|
261
316
|
)
|
|
262
|
-
|
|
317
|
+
# Get API URL suggestions and create suggester
|
|
318
|
+
url_suggestions = self._get_api_url_suggestions("")
|
|
319
|
+
url_suggester = None
|
|
320
|
+
if SuggestFromList and url_suggestions:
|
|
321
|
+
url_suggester = SuggestFromList(url_suggestions, case_sensitive=False)
|
|
322
|
+
url_input = Input(
|
|
323
|
+
value=self._existing.get("api_url", ""),
|
|
324
|
+
placeholder="https://api.example.com",
|
|
325
|
+
id="form-url",
|
|
326
|
+
suggester=url_suggester,
|
|
327
|
+
)
|
|
263
328
|
key_input = Input(value="", placeholder="sk-...", password=True, id="form-key")
|
|
264
329
|
test_checkbox = Checkbox(
|
|
265
330
|
"Test connection before save",
|
|
@@ -427,8 +492,13 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
427
492
|
self._env_lock = env_lock
|
|
428
493
|
self._callbacks = callbacks
|
|
429
494
|
self._ctx = ctx
|
|
495
|
+
self._keybinds: KeybindRegistry | None = None
|
|
496
|
+
self._toast_bus: ToastBus | None = None
|
|
497
|
+
self._toast_ready = False
|
|
498
|
+
self._clipboard: ClipboardAdapter | None = None
|
|
430
499
|
self._filter_text: str = ""
|
|
431
500
|
self._is_switching = False
|
|
501
|
+
self._initialize_context_services()
|
|
432
502
|
|
|
433
503
|
def compose(self) -> ComposeResult:
|
|
434
504
|
"""Build the Textual layout."""
|
|
@@ -481,6 +551,52 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
481
551
|
main = self.query_one(Vertical)
|
|
482
552
|
main.styles.gap = 0
|
|
483
553
|
self._update_filter_button_visibility()
|
|
554
|
+
self._prepare_toasts()
|
|
555
|
+
self._register_keybinds()
|
|
556
|
+
|
|
557
|
+
def _initialize_context_services(self) -> None:
|
|
558
|
+
if self._ctx:
|
|
559
|
+
if self._ctx.keybinds is None:
|
|
560
|
+
self._ctx.keybinds = KeybindRegistry()
|
|
561
|
+
if self._ctx.toasts is None:
|
|
562
|
+
self._ctx.toasts = ToastBus()
|
|
563
|
+
if self._ctx.clipboard is None:
|
|
564
|
+
self._ctx.clipboard = ClipboardAdapter(terminal=self._ctx.terminal)
|
|
565
|
+
self._keybinds = self._ctx.keybinds
|
|
566
|
+
self._toast_bus = self._ctx.toasts
|
|
567
|
+
self._clipboard = self._ctx.clipboard
|
|
568
|
+
else:
|
|
569
|
+
# Fallback: create services independently when ctx is None
|
|
570
|
+
terminal = TerminalCapabilities(
|
|
571
|
+
tty=True, ansi=True, osc52=False, osc11_bg=None, mouse=False, truecolor=False
|
|
572
|
+
)
|
|
573
|
+
self._clipboard = ClipboardAdapter(terminal=terminal)
|
|
574
|
+
|
|
575
|
+
def _prepare_toasts(self) -> None:
|
|
576
|
+
"""Prepare toast system by marking ready and clearing any existing toasts."""
|
|
577
|
+
self._toast_ready = True
|
|
578
|
+
if self._toast_bus:
|
|
579
|
+
self._toast_bus.clear()
|
|
580
|
+
|
|
581
|
+
def _register_keybinds(self) -> None:
|
|
582
|
+
if not self._keybinds:
|
|
583
|
+
return
|
|
584
|
+
for keybind_def in KEYBIND_DEFINITIONS:
|
|
585
|
+
scoped_action = f"{KEYBIND_SCOPE}.{keybind_def.action}"
|
|
586
|
+
if self._keybinds.get(scoped_action):
|
|
587
|
+
continue
|
|
588
|
+
try:
|
|
589
|
+
self._keybinds.register(
|
|
590
|
+
action=scoped_action,
|
|
591
|
+
key=keybind_def.key,
|
|
592
|
+
description=keybind_def.description,
|
|
593
|
+
category=KEYBIND_CATEGORY,
|
|
594
|
+
)
|
|
595
|
+
except ValueError as e:
|
|
596
|
+
# Expected: duplicate registration (already registered by another component)
|
|
597
|
+
# Silently skip to allow multiple apps to register same keybinds
|
|
598
|
+
logging.debug(f"Skipping duplicate keybind registration: {scoped_action}", exc_info=e)
|
|
599
|
+
continue
|
|
484
600
|
|
|
485
601
|
def _header_text(self) -> str:
|
|
486
602
|
"""Build header text with active account and host."""
|
|
@@ -643,6 +759,24 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
643
759
|
"""Hide the loading indicator."""
|
|
644
760
|
hide_loading_indicator(self, ACCOUNTS_LOADING_ID)
|
|
645
761
|
|
|
762
|
+
def _handle_switch_scheduling_error(self, exc: Exception) -> None:
|
|
763
|
+
"""Handle errors when scheduling the switch task fails.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
exc: The exception that occurred during task scheduling.
|
|
767
|
+
"""
|
|
768
|
+
self._hide_loading()
|
|
769
|
+
self._is_switching = False
|
|
770
|
+
error_msg = f"Switch failed to start: {exc}"
|
|
771
|
+
if self._toast_ready and self._toast_bus:
|
|
772
|
+
self._toast_bus.show(message=error_msg, variant="error")
|
|
773
|
+
try:
|
|
774
|
+
self._set_status(error_msg, "red")
|
|
775
|
+
except Exception:
|
|
776
|
+
# App not mounted yet, status update not possible
|
|
777
|
+
logging.error(error_msg, exc_info=exc)
|
|
778
|
+
logging.getLogger(__name__).debug("Failed to schedule switch task", exc_info=exc)
|
|
779
|
+
|
|
646
780
|
def _clear_filter(self) -> None:
|
|
647
781
|
"""Clear the filter input and reset filter state."""
|
|
648
782
|
filter_input = self.query_one(FILTER_INPUT_ID, Input)
|
|
@@ -665,7 +799,9 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
665
799
|
|
|
666
800
|
if switched:
|
|
667
801
|
self._active_account = name
|
|
668
|
-
|
|
802
|
+
status_msg = message or f"Switched to '{name}'."
|
|
803
|
+
if self._toast_ready and self._toast_bus:
|
|
804
|
+
self._toast_bus.show(message=status_msg, variant="success")
|
|
669
805
|
self._update_header()
|
|
670
806
|
self._reload_rows()
|
|
671
807
|
else:
|
|
@@ -674,11 +810,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
674
810
|
try:
|
|
675
811
|
self.track_task(perform(), logger=logging.getLogger(__name__))
|
|
676
812
|
except Exception as exc:
|
|
677
|
-
|
|
678
|
-
self._hide_loading()
|
|
679
|
-
self._is_switching = False
|
|
680
|
-
self._set_status(f"Switch failed to start: {exc}", "red")
|
|
681
|
-
logging.getLogger(__name__).debug("Failed to schedule switch task", exc_info=exc)
|
|
813
|
+
self._handle_switch_scheduling_error(exc)
|
|
682
814
|
|
|
683
815
|
def _update_header(self) -> None:
|
|
684
816
|
"""Refresh header text to reflect active/lock state."""
|
|
@@ -781,12 +913,32 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
781
913
|
return
|
|
782
914
|
|
|
783
915
|
text = f"Account: {name}\nURL: {account.get('api_url', '')}"
|
|
784
|
-
adapter = ClipboardAdapter()
|
|
785
|
-
|
|
916
|
+
adapter = self._clipboard or ClipboardAdapter(terminal=self._ctx.terminal if self._ctx else None)
|
|
917
|
+
# OSC 52 works by writing to stdout, no custom writer needed
|
|
918
|
+
try:
|
|
919
|
+
asyncio.get_running_loop()
|
|
920
|
+
except RuntimeError:
|
|
921
|
+
result = adapter.copy(text)
|
|
922
|
+
self._handle_copy_result(name, result)
|
|
923
|
+
return
|
|
924
|
+
|
|
925
|
+
async def perform() -> None:
|
|
926
|
+
result = await asyncio.to_thread(adapter.copy, text)
|
|
927
|
+
self._handle_copy_result(name, result)
|
|
928
|
+
|
|
929
|
+
self.track_task(perform(), logger=logging.getLogger(__name__))
|
|
786
930
|
|
|
931
|
+
def _handle_copy_result(self, name: str, result: ClipboardResult) -> None:
|
|
932
|
+
"""Update UI state after a copy attempt."""
|
|
787
933
|
if result.success:
|
|
934
|
+
if self._toast_ready and self._toast_bus:
|
|
935
|
+
self._toast_bus.copy_success(label=name)
|
|
936
|
+
# Status fallback until toast widget is implemented (see specs/workflow/tui-toast-system/spec.md Phase 2)
|
|
788
937
|
self._set_status(f"Copied '{name}' to clipboard.", "green")
|
|
789
938
|
else:
|
|
939
|
+
if self._toast_ready and self._toast_bus:
|
|
940
|
+
self._toast_bus.show(message=f"Copy failed: {result.message}", variant="warning")
|
|
941
|
+
# Status fallback until toast widget is implemented (see specs/workflow/tui-toast-system/spec.md Phase 2)
|
|
790
942
|
self._set_status(f"Copy failed: {result.message}", "red")
|
|
791
943
|
|
|
792
944
|
def _check_env_lock_hotkey(self) -> bool:
|
|
@@ -12,13 +12,14 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import os
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
-
from typing import TYPE_CHECKING
|
|
16
15
|
|
|
16
|
+
from glaip_sdk.cli.account_store import get_account_store
|
|
17
|
+
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
|
|
18
|
+
from glaip_sdk.cli.slash.tui.keybind_registry import KeybindRegistry
|
|
17
19
|
from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
|
|
18
20
|
from glaip_sdk.cli.slash.tui.theme import ThemeManager
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
from glaip_sdk.cli.slash.tui.toast import ToastBus
|
|
21
|
+
from glaip_sdk.cli.slash.tui.toast import ToastBus
|
|
22
|
+
from glaip_sdk.cli.tui_settings import load_tui_settings
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@dataclass
|
|
@@ -37,23 +38,47 @@ class TUIContext:
|
|
|
37
38
|
"""
|
|
38
39
|
|
|
39
40
|
terminal: TerminalCapabilities
|
|
40
|
-
keybinds:
|
|
41
|
+
keybinds: KeybindRegistry | None = None
|
|
41
42
|
theme: ThemeManager | None = None
|
|
42
43
|
toasts: ToastBus | None = None
|
|
43
|
-
clipboard:
|
|
44
|
+
clipboard: ClipboardAdapter | None = None
|
|
44
45
|
|
|
45
46
|
@classmethod
|
|
46
47
|
async def create(cls) -> TUIContext:
|
|
47
48
|
"""Create a TUIContext instance with detected terminal capabilities.
|
|
48
49
|
|
|
49
50
|
This factory method detects terminal capabilities asynchronously and
|
|
50
|
-
returns a populated TUIContext instance
|
|
51
|
-
theme, toasts, clipboard)
|
|
51
|
+
returns a populated TUIContext instance with all services initialized
|
|
52
|
+
(keybinds, theme, toasts, clipboard).
|
|
52
53
|
|
|
53
54
|
Returns:
|
|
54
|
-
TUIContext instance with
|
|
55
|
+
TUIContext instance with all services initialized.
|
|
55
56
|
"""
|
|
56
57
|
terminal = await TerminalCapabilities.detect()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
store = get_account_store()
|
|
59
|
+
settings = load_tui_settings(store=store)
|
|
60
|
+
|
|
61
|
+
# Handle env var override: normalize empty strings and "default" to None
|
|
62
|
+
# Empty string from os.getenv() is falsy, so strip() result becomes None in the or expression
|
|
63
|
+
env_theme = os.getenv("AIP_TUI_THEME")
|
|
64
|
+
env_theme = env_theme.strip() if env_theme else None
|
|
65
|
+
if env_theme and env_theme.lower() == "default":
|
|
66
|
+
env_theme = None
|
|
67
|
+
|
|
68
|
+
theme_name = env_theme or settings.theme_name
|
|
69
|
+
theme = ThemeManager(
|
|
70
|
+
terminal,
|
|
71
|
+
mode=settings.theme_mode,
|
|
72
|
+
theme=theme_name,
|
|
73
|
+
settings_store=store,
|
|
74
|
+
)
|
|
75
|
+
keybinds = KeybindRegistry()
|
|
76
|
+
toasts = ToastBus()
|
|
77
|
+
clipboard = ClipboardAdapter(terminal=terminal)
|
|
78
|
+
return cls(
|
|
79
|
+
terminal=terminal,
|
|
80
|
+
keybinds=keybinds,
|
|
81
|
+
theme=theme,
|
|
82
|
+
toasts=toasts,
|
|
83
|
+
clipboard=clipboard,
|
|
84
|
+
)
|
|
@@ -6,9 +6,11 @@ import logging
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Literal
|
|
8
8
|
|
|
9
|
+
from glaip_sdk.cli.account_store import AccountStore, AccountStoreError
|
|
9
10
|
from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
|
|
10
11
|
from glaip_sdk.cli.slash.tui.theme.catalog import default_theme_name_for_mode, get_builtin_theme
|
|
11
12
|
from glaip_sdk.cli.slash.tui.theme.tokens import ThemeTokens
|
|
13
|
+
from glaip_sdk.cli.tui_settings import persist_tui_theme
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
@@ -30,11 +32,13 @@ class ThemeManager:
|
|
|
30
32
|
*,
|
|
31
33
|
mode: ThemeMode | str = ThemeMode.AUTO,
|
|
32
34
|
theme: str | None = None,
|
|
35
|
+
settings_store: AccountStore | None = None,
|
|
33
36
|
) -> None:
|
|
34
37
|
"""Initialize the theme manager."""
|
|
35
38
|
self._terminal = terminal
|
|
36
39
|
self._mode = self._coerce_mode(mode)
|
|
37
|
-
self._theme = theme
|
|
40
|
+
self._theme = self._normalize_theme_name(theme)
|
|
41
|
+
self._settings_store = settings_store
|
|
38
42
|
|
|
39
43
|
@property
|
|
40
44
|
def mode(self) -> ThemeMode:
|
|
@@ -70,10 +74,12 @@ class ThemeManager:
|
|
|
70
74
|
def set_mode(self, mode: ThemeMode | str) -> None:
|
|
71
75
|
"""Set auto/light/dark mode."""
|
|
72
76
|
self._mode = self._coerce_mode(mode)
|
|
77
|
+
self._persist_preferences()
|
|
73
78
|
|
|
74
79
|
def set_theme(self, theme: str | None) -> None:
|
|
75
80
|
"""Set explicit theme name (or None to use the default)."""
|
|
76
|
-
self._theme = theme
|
|
81
|
+
self._theme = self._normalize_theme_name(theme)
|
|
82
|
+
self._persist_preferences()
|
|
77
83
|
|
|
78
84
|
def _coerce_mode(self, mode: ThemeMode | str) -> ThemeMode:
|
|
79
85
|
"""Coerce a mode value to ThemeMode enum, defaulting to AUTO on invalid input."""
|
|
@@ -84,3 +90,23 @@ class ThemeManager:
|
|
|
84
90
|
except ValueError:
|
|
85
91
|
logger.warning(f"Invalid theme mode '{mode}', defaulting to AUTO")
|
|
86
92
|
return ThemeMode.AUTO
|
|
93
|
+
|
|
94
|
+
def _persist_preferences(self) -> None:
|
|
95
|
+
if self._settings_store is None:
|
|
96
|
+
return
|
|
97
|
+
try:
|
|
98
|
+
persist_tui_theme(mode=self._mode.value, name=self._theme, store=self._settings_store)
|
|
99
|
+
except (OSError, AccountStoreError) as exc:
|
|
100
|
+
# Log recoverable errors (permissions, I/O) as warnings
|
|
101
|
+
logger.warning(f"Failed to persist TUI theme preferences: {exc}")
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
# Log unexpected errors at error level for debugging
|
|
104
|
+
logger.error(f"Unexpected error persisting TUI theme preferences: {exc}", exc_info=True)
|
|
105
|
+
|
|
106
|
+
def _normalize_theme_name(self, theme: str | None) -> str | None:
|
|
107
|
+
if not isinstance(theme, str):
|
|
108
|
+
return None
|
|
109
|
+
cleaned = theme.strip()
|
|
110
|
+
if not cleaned or cleaned.lower() == "default":
|
|
111
|
+
return None
|
|
112
|
+
return cleaned
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Typed loader/saver for TUI preferences stored in config.yaml.
|
|
2
|
+
|
|
3
|
+
This module implements the TUI preferences store as defined in
|
|
4
|
+
`specs/architecture/tui-preferences-store/spec.md`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Literal, cast
|
|
11
|
+
|
|
12
|
+
from glaip_sdk.cli.account_store import AccountStore, get_account_store
|
|
13
|
+
|
|
14
|
+
ThemeModeValue = Literal["auto", "light", "dark"]
|
|
15
|
+
|
|
16
|
+
_DEFAULT_THEME_MODE: ThemeModeValue = "auto"
|
|
17
|
+
_DEFAULT_LEADER = "ctrl+x"
|
|
18
|
+
_DEFAULT_MOUSE_CAPTURE = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class TUISettings:
|
|
23
|
+
"""Resolved TUI preferences from config.yaml."""
|
|
24
|
+
|
|
25
|
+
theme_mode: ThemeModeValue = _DEFAULT_THEME_MODE
|
|
26
|
+
theme_name: str | None = None
|
|
27
|
+
leader: str = _DEFAULT_LEADER
|
|
28
|
+
keybind_overrides: dict[str, str] = field(default_factory=dict)
|
|
29
|
+
mouse_capture: bool = _DEFAULT_MOUSE_CAPTURE
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_tui_settings(*, store: AccountStore | None = None) -> TUISettings:
|
|
33
|
+
"""Load TUI preferences from the CLI config file."""
|
|
34
|
+
store = store or get_account_store()
|
|
35
|
+
config = store.load_config()
|
|
36
|
+
|
|
37
|
+
tui = _as_dict(config.get("tui"))
|
|
38
|
+
theme = _as_dict(tui.get("theme"))
|
|
39
|
+
mode = _coerce_theme_mode(theme.get("mode"))
|
|
40
|
+
name = _normalize_theme_name(theme.get("name"))
|
|
41
|
+
|
|
42
|
+
keybinds = _as_dict(tui.get("keybinds"))
|
|
43
|
+
leader = keybinds.get("leader")
|
|
44
|
+
if not isinstance(leader, str) or not leader.strip():
|
|
45
|
+
leader = _DEFAULT_LEADER
|
|
46
|
+
|
|
47
|
+
overrides = _coerce_keybind_overrides(keybinds.get("overrides"))
|
|
48
|
+
|
|
49
|
+
mouse_capture = tui.get("mouse_capture")
|
|
50
|
+
if not isinstance(mouse_capture, bool):
|
|
51
|
+
mouse_capture = _DEFAULT_MOUSE_CAPTURE
|
|
52
|
+
|
|
53
|
+
return TUISettings(
|
|
54
|
+
theme_mode=mode,
|
|
55
|
+
theme_name=name,
|
|
56
|
+
leader=leader,
|
|
57
|
+
keybind_overrides=overrides,
|
|
58
|
+
mouse_capture=mouse_capture,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def update_tui_settings(patch: dict[str, Any], *, store: AccountStore | None = None) -> None:
|
|
63
|
+
"""Update TUI preferences, preserving unrelated config keys."""
|
|
64
|
+
store = store or get_account_store()
|
|
65
|
+
config = store.load_config()
|
|
66
|
+
|
|
67
|
+
existing = _as_dict(config.get("tui"))
|
|
68
|
+
config["tui"] = _merge_dict(existing, patch)
|
|
69
|
+
|
|
70
|
+
store.save_config_updates(config)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def persist_tui_theme(*, mode: ThemeModeValue, name: str | None, store: AccountStore | None = None) -> None:
|
|
74
|
+
"""Persist theme preferences in the tui.theme namespace."""
|
|
75
|
+
update_tui_settings(
|
|
76
|
+
{"theme": {"mode": _coerce_theme_mode(mode), "name": _serialize_theme_name(name)}},
|
|
77
|
+
store=store,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _as_dict(value: Any) -> dict[str, Any]:
|
|
82
|
+
if isinstance(value, dict):
|
|
83
|
+
return dict(value)
|
|
84
|
+
return {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _coerce_theme_mode(value: Any) -> ThemeModeValue:
|
|
88
|
+
if isinstance(value, str):
|
|
89
|
+
lowered = value.strip().lower()
|
|
90
|
+
if lowered in {"auto", "light", "dark"}:
|
|
91
|
+
return cast(ThemeModeValue, lowered)
|
|
92
|
+
return _DEFAULT_THEME_MODE
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalize_theme_name(value: Any) -> str | None:
|
|
96
|
+
if not isinstance(value, str):
|
|
97
|
+
return None
|
|
98
|
+
cleaned = value.strip()
|
|
99
|
+
if not cleaned or cleaned.lower() == "default":
|
|
100
|
+
return None
|
|
101
|
+
return cleaned
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _serialize_theme_name(name: str | None) -> str:
|
|
105
|
+
if isinstance(name, str):
|
|
106
|
+
cleaned = name.strip()
|
|
107
|
+
if cleaned:
|
|
108
|
+
return cleaned
|
|
109
|
+
return "default"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _coerce_keybind_overrides(value: Any) -> dict[str, str]:
|
|
113
|
+
if not isinstance(value, dict):
|
|
114
|
+
return {}
|
|
115
|
+
return {key: val for key, val in value.items() if isinstance(key, str) and isinstance(val, str)}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _merge_dict(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
119
|
+
merged = dict(base)
|
|
120
|
+
for key, value in patch.items():
|
|
121
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
122
|
+
merged[key] = _merge_dict(cast(dict[str, Any], merged[key]), value)
|
|
123
|
+
else:
|
|
124
|
+
merged[key] = value
|
|
125
|
+
return merged
|
glaip_sdk/tools/base.py
CHANGED
|
@@ -387,12 +387,22 @@ class Tool:
|
|
|
387
387
|
if not self._client:
|
|
388
388
|
raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
|
|
389
389
|
|
|
390
|
+
# Handle both Client (has .tools) and ToolClient (direct methods)
|
|
391
|
+
# Priority: Check if client has a 'tools' attribute (Client instance)
|
|
392
|
+
# Otherwise, use client directly (ToolClient instance)
|
|
393
|
+
if hasattr(self._client, "tools") and self._client.tools is not None:
|
|
394
|
+
# Main Client instance - use the tools sub-client
|
|
395
|
+
tools_client = self._client.tools
|
|
396
|
+
else:
|
|
397
|
+
# ToolClient instance - use directly
|
|
398
|
+
tools_client = self._client
|
|
399
|
+
|
|
390
400
|
# Check if file upload is requested
|
|
391
401
|
if "file" in kwargs:
|
|
392
402
|
file_path = kwargs.pop("file")
|
|
393
|
-
response =
|
|
403
|
+
response = tools_client.update_tool_via_file(self._id, file_path, **kwargs)
|
|
394
404
|
else:
|
|
395
|
-
response =
|
|
405
|
+
response = tools_client.update_tool(tool_id=self._id, **kwargs)
|
|
396
406
|
|
|
397
407
|
# Update local properties from response
|
|
398
408
|
if hasattr(response, "name") and response.name:
|
|
@@ -416,7 +426,17 @@ class Tool:
|
|
|
416
426
|
if not self._client:
|
|
417
427
|
raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
|
|
418
428
|
|
|
419
|
-
|
|
429
|
+
# Handle both Client (has .tools) and ToolClient (direct methods)
|
|
430
|
+
# Priority: Check if client has a 'tools' attribute (Client instance)
|
|
431
|
+
# Otherwise, use client directly (ToolClient instance)
|
|
432
|
+
if hasattr(self._client, "tools") and self._client.tools is not None:
|
|
433
|
+
# Main Client instance - use the tools sub-client
|
|
434
|
+
tools_client = self._client.tools
|
|
435
|
+
else:
|
|
436
|
+
# ToolClient instance - use directly
|
|
437
|
+
tools_client = self._client
|
|
438
|
+
|
|
439
|
+
tools_client.delete_tool(self._id)
|
|
420
440
|
self._id = None
|
|
421
441
|
self._client = None
|
|
422
442
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glaip-sdk
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.9
|
|
4
4
|
Summary: Python SDK and CLI for GL AIP (GDP Labs AI Agent Package) - Build, run, and manage AI agents
|
|
5
5
|
Author-email: Raymond Christopher <raymond.christopher@gdplabs.id>
|
|
6
6
|
License: MIT
|
|
@@ -20,11 +20,11 @@ Requires-Dist: gllm-core-binary>=0.1.0
|
|
|
20
20
|
Requires-Dist: langchain-core>=0.3.0
|
|
21
21
|
Requires-Dist: gllm-tools-binary>=0.1.3
|
|
22
22
|
Provides-Extra: local
|
|
23
|
-
Requires-Dist: aip-agents-binary[local]>=0.5.
|
|
23
|
+
Requires-Dist: aip-agents-binary[local]>=0.5.23; (python_version >= "3.11" and python_version < "3.13") and extra == "local"
|
|
24
24
|
Provides-Extra: memory
|
|
25
|
-
Requires-Dist: aip-agents-binary[memory]>=0.5.
|
|
25
|
+
Requires-Dist: aip-agents-binary[memory]>=0.5.23; (python_version >= "3.11" and python_version < "3.13") and extra == "memory"
|
|
26
26
|
Provides-Extra: privacy
|
|
27
|
-
Requires-Dist: aip-agents-binary[privacy]>=0.5.
|
|
27
|
+
Requires-Dist: aip-agents-binary[privacy]>=0.5.23; (python_version >= "3.11" and python_version < "3.13") and extra == "privacy"
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
30
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -7,10 +7,10 @@ glaip_sdk/rich_components.py,sha256=44Z0V1ZQleVh9gUDGwRR5mriiYFnVGOhm7fFxZYbP8c,
|
|
|
7
7
|
glaip_sdk/agents/__init__.py,sha256=VfYov56edbWuySXFEbWJ_jLXgwnFzPk1KB-9-mfsUCc,776
|
|
8
8
|
glaip_sdk/agents/base.py,sha256=GQnzCw2cqlrbxwdoWFfhBcBlEDgubY4tlD6gr1b3zps,44539
|
|
9
9
|
glaip_sdk/cli/__init__.py,sha256=xCCfuF1Yc7mpCDcfhHZTX0vizvtrDSLeT8MJ3V7m5A0,156
|
|
10
|
-
glaip_sdk/cli/account_store.py,sha256=
|
|
10
|
+
glaip_sdk/cli/account_store.py,sha256=u_memecwEQssustZs2wYBrHbEmKUlDfmmL-zO1F3n3A,19034
|
|
11
11
|
glaip_sdk/cli/agent_config.py,sha256=YAbFKrTNTRqNA6b0i0Q3pH-01rhHDRi5v8dxSFwGSwM,2401
|
|
12
12
|
glaip_sdk/cli/auth.py,sha256=bqOHMGIOCg3KXssme3uJBBjEbK0rCEppQ6oq-gJ-hzA,24276
|
|
13
|
-
glaip_sdk/cli/config.py,sha256=
|
|
13
|
+
glaip_sdk/cli/config.py,sha256=hFKQSdqELQZLKBuFnhEirNLDyPJwEmpQuD8nHuWxokg,3051
|
|
14
14
|
glaip_sdk/cli/constants.py,sha256=zqcVtzfj6huW97gbCmhkFqntge1H-c1vnkGqTazADgU,895
|
|
15
15
|
glaip_sdk/cli/context.py,sha256=--Y5vc6lgoAV7cRoUAr9UxSQaLmkMg29FolA7EwoRqM,3803
|
|
16
16
|
glaip_sdk/cli/display.py,sha256=ojgWdGeD5KUnGOmWNqqK4JP-1EaWHWX--DWze3BmIz0,12137
|
|
@@ -23,6 +23,7 @@ glaip_sdk/cli/mcp_validators.py,sha256=cwbz7p_p7_9xVuuF96OBQOdmEgo5UObU6iWWQ2X03
|
|
|
23
23
|
glaip_sdk/cli/pager.py,sha256=TmiMDNpUMuZju7QJ6A_ITqIoEf8Dhv8U6mTXx2Fga1k,7935
|
|
24
24
|
glaip_sdk/cli/resolution.py,sha256=AGvv7kllLcuvk_jdaArJqH3lId4IDEXpHceRZwy14xY,2448
|
|
25
25
|
glaip_sdk/cli/rich_helpers.py,sha256=kO47N8e506rxrN6Oc9mbAWN3Qb536oQPWZy1s9A616g,819
|
|
26
|
+
glaip_sdk/cli/tui_settings.py,sha256=ey9bSlolEj3_SMmjVVV_PY0FkfH1psAFPOl03NlykvI,3867
|
|
26
27
|
glaip_sdk/cli/update_notifier.py,sha256=0zpWxr4nSyz0tiLWyC7EEO2deAnVmsRcVlMV79G2QMI,18049
|
|
27
28
|
glaip_sdk/cli/validators.py,sha256=k4J2ACYJPF6UmWJfENt9OHWdp4RNArVxR3hoeqauO88,5629
|
|
28
29
|
glaip_sdk/cli/commands/__init__.py,sha256=6Z3ASXDut0lAbUX_umBFtxPzzFyqoiZfVeTahThFu1A,219
|
|
@@ -80,10 +81,10 @@ glaip_sdk/cli/slash/remote_runs_controller.py,sha256=a5X5rYgb9l6dHhvTewRUCj-hAo7
|
|
|
80
81
|
glaip_sdk/cli/slash/session.py,sha256=Zn2hXND_Tfameh_PI8g4VIMd7GPWxwhtPNMN9p6cF7M,65081
|
|
81
82
|
glaip_sdk/cli/slash/tui/__init__.py,sha256=oBUzeoslYwPKVlhqhgg4I7480b77vQNc9ec0NgdTC1s,977
|
|
82
83
|
glaip_sdk/cli/slash/tui/accounts.tcss,sha256=BCjIuTetmVjydv6DCliY38Cze2LUEu7IY44sL5nIuLU,1194
|
|
83
|
-
glaip_sdk/cli/slash/tui/accounts_app.py,sha256=
|
|
84
|
+
glaip_sdk/cli/slash/tui/accounts_app.py,sha256=pQYQz9ljPn4WcXEmyhMk3Lt1h_ZgrMvlYplxN3lbcy4,42411
|
|
84
85
|
glaip_sdk/cli/slash/tui/background_tasks.py,sha256=SAe1mV2vXB3mJcSGhelU950vf8Lifjhws9iomyIVFKw,2422
|
|
85
86
|
glaip_sdk/cli/slash/tui/clipboard.py,sha256=HL_RWIdONyRmDtTYuDzxJTS_mRcLxuR37Ac9Ug5nh40,4730
|
|
86
|
-
glaip_sdk/cli/slash/tui/context.py,sha256=
|
|
87
|
+
glaip_sdk/cli/slash/tui/context.py,sha256=XgHsXPl8LDDwIueP_jhP6xqpx_zHOk0FF19rWob_z5Q,3128
|
|
87
88
|
glaip_sdk/cli/slash/tui/keybind_registry.py,sha256=_rK05BxTxNudYc4iJ9gDxpgeUkjDAq8rarIT-9A-jyM,6739
|
|
88
89
|
glaip_sdk/cli/slash/tui/loading.py,sha256=nW5pv_Tnl9FUOPR3Qf2O5gt1AGHSo3b5-Uofg34F6AE,1909
|
|
89
90
|
glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=RCrI-c5ilKV6Iy1lz2Aok9xo2Ou02vqcXACMXTdodnE,24716
|
|
@@ -91,7 +92,7 @@ glaip_sdk/cli/slash/tui/terminal.py,sha256=iC31XChTL34gXY6vXdSIX3HmD36tuA9EYTPZ2
|
|
|
91
92
|
glaip_sdk/cli/slash/tui/toast.py,sha256=LP_myZwgnrdowrRxGK24lMlx7iZt7iOwFhrbc4NW0DY,3493
|
|
92
93
|
glaip_sdk/cli/slash/tui/theme/__init__.py,sha256=rtM2ik83YNCRcI1qh_Sf3rnxco2OvCNNT3NbHY6cLvw,432
|
|
93
94
|
glaip_sdk/cli/slash/tui/theme/catalog.py,sha256=G52eU3h8YI9D8XUALVg1KVZ4Lq65VnZdgPS3F_P7XLE,2544
|
|
94
|
-
glaip_sdk/cli/slash/tui/theme/manager.py,sha256=
|
|
95
|
+
glaip_sdk/cli/slash/tui/theme/manager.py,sha256=LBnxEMIwz-8cAlZGYk5tIoAJbOJyGYsmDlyuGJ-LlX4,3945
|
|
95
96
|
glaip_sdk/cli/slash/tui/theme/tokens.py,sha256=ympMRny_d-gHtmnPR-lmNZ-C9SGBy2q-MH81l0L1h-Y,1423
|
|
96
97
|
glaip_sdk/cli/transcript/__init__.py,sha256=yiYHyNtebMCu3BXu56Xm5RBC2tDc865q8UGPnoe6QRs,920
|
|
97
98
|
glaip_sdk/cli/transcript/cache.py,sha256=Wi1uln6HP1U6F-MRTrfnxi9bn6XJTxwWXhREIRPoMqQ,17439
|
|
@@ -153,7 +154,7 @@ glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py,sha256=SgfQM5NgKyYBs34ju
|
|
|
153
154
|
glaip_sdk/schedules/__init__.py,sha256=Ty__lE8ta3a6O7EiEsSXliVOwA3EBLKxKRsjAJt2WUg,482
|
|
154
155
|
glaip_sdk/schedules/base.py,sha256=ZRKWknoxQOYMhX8mjQ7S7oqpy6Wr0xdbzcgIrycsEQ8,9727
|
|
155
156
|
glaip_sdk/tools/__init__.py,sha256=rhGzEqQFCzeMrxmikBuNrMz4PyYczwic28boDKVmoHs,585
|
|
156
|
-
glaip_sdk/tools/base.py,sha256=
|
|
157
|
+
glaip_sdk/tools/base.py,sha256=KRaWWX5cKAvEKtBr4iSOaKQlQ973A4pNOW2KVvA1aYs,17353
|
|
157
158
|
glaip_sdk/utils/__init__.py,sha256=5a1kNLtUriwd1qAT6RU083GOyABS7LMZQacDP4yS9S4,2830
|
|
158
159
|
glaip_sdk/utils/agent_config.py,sha256=RhcHsSOVwOaSC2ggnPuHn36Aa0keGJhs8KGb2InvzRk,7262
|
|
159
160
|
glaip_sdk/utils/bundler.py,sha256=fLumFj1MqqqGA1Mwn05v_cEKPALv3rIPEMvaURpxZ80,15171
|
|
@@ -206,8 +207,8 @@ glaip_sdk/utils/rendering/steps/format.py,sha256=Chnq7OBaj8XMeBntSBxrX5zSmrYeGcO
|
|
|
206
207
|
glaip_sdk/utils/rendering/steps/manager.py,sha256=BiBmTeQMQhjRMykgICXsXNYh1hGsss-fH9BIGVMWFi0,13194
|
|
207
208
|
glaip_sdk/utils/rendering/viewer/__init__.py,sha256=XrxmE2cMAozqrzo1jtDFm8HqNtvDcYi2mAhXLXn5CjI,457
|
|
208
209
|
glaip_sdk/utils/rendering/viewer/presenter.py,sha256=mlLMTjnyeyPVtsyrAbz1BJu9lFGQSlS-voZ-_Cuugv0,5725
|
|
209
|
-
glaip_sdk-0.7.
|
|
210
|
-
glaip_sdk-0.7.
|
|
211
|
-
glaip_sdk-0.7.
|
|
212
|
-
glaip_sdk-0.7.
|
|
213
|
-
glaip_sdk-0.7.
|
|
210
|
+
glaip_sdk-0.7.9.dist-info/METADATA,sha256=Zv0LWJWtHBnmVcDY19GixzfzpVEIGpTUEQeqInAMd5E,8365
|
|
211
|
+
glaip_sdk-0.7.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
212
|
+
glaip_sdk-0.7.9.dist-info/entry_points.txt,sha256=NkhO6FfgX9Zrjn63GuKphf-dLw7KNJvucAcXc7P3aMk,54
|
|
213
|
+
glaip_sdk-0.7.9.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
|
|
214
|
+
glaip_sdk-0.7.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|