Skip to content

Commit 482bd12

Browse files
committed
refactor(BA-5528): move print_summary onto entry, split clear, drop raw dict iter
Address review comments on PR #11344: - types.py: add DeploymentChatCacheEntry.print_summary classmethod that owns the full per-deployment display (deployment_id header + entry fields + masked api_key line). The caller passes a pre-masked token string so the type stays free of the mask_token helper. The free function _print_entry in commands.py is gone. - commands.py: split chat-config clear into chat-config clear-cache and chat-config clear-token. One name doing two jobs (wipe endpoint cache AND user-supplied token) was misleading; each subcommand now touches exactly one store. - utils.py: replace the manual deployments / tokens dict-walking + per-entry model_validate with a single DeploymentChatCache.model_validate / DeploymentChatConfig.model_validate call. Pydantic now does both UUID key parsing and entry validation; per-record resilience downgrades to per-file resilience (a tampered file falls back to an empty store). Tests retire the per-entry skip cases since the loader is fail-all-or- none now, but still verify that an invalid UUID key or malformed entry yields an empty store rather than crashing.
1 parent cdbb40f commit 482bd12

4 files changed

Lines changed: 50 additions & 86 deletions

File tree

src/ai/backend/client/cli/v2/deployment/chat/commands.py

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -261,42 +261,31 @@ def show(deployment_id: UUID) -> None:
261261
token = chat_config_store.get_token(deployment_id)
262262
if entry is None and token is None:
263263
raise click.ClickException(f"No chat cache entry for deployment {deployment_id}.")
264-
_print_entry(deployment_id, entry, token)
264+
DeploymentChatCacheEntry.print_summary(deployment_id, entry, mask_token(token))
265265

266266

267-
@chat_config.command(name="clear")
267+
@chat_config.command(name="clear-cache")
268268
@click.argument("deployment_id", type=click.UUID)
269-
def clear(deployment_id: UUID) -> None:
270-
"""Remove the chat cache entry and stored token for a deployment."""
269+
def clear_cache(deployment_id: UUID) -> None:
270+
"""Remove the cached endpoint entry for a deployment."""
271271
cache = load_chat_cache()
272-
chat_config_store = load_chat_config()
273-
274-
removed_entry = cache.remove(deployment_id)
275-
removed_token = chat_config_store.clear_token(deployment_id)
276-
if removed_entry:
272+
if cache.remove(deployment_id):
277273
save_chat_cache(cache)
278-
if removed_token:
279-
save_chat_config(chat_config_store)
280-
if removed_entry or removed_token:
281-
print(f"Removed chat cache entry for deployment {deployment_id}.")
274+
print(f"Removed cache entry for deployment {deployment_id}.")
282275
else:
283-
print(f"No chat cache entry for deployment {deployment_id}.")
276+
print(f"No cache entry for deployment {deployment_id}.")
284277

285278

286-
def _print_entry(
287-
deployment_id: UUID,
288-
entry: DeploymentChatCacheEntry | None,
289-
token: str | None,
290-
) -> None:
291-
print(f"deployment_id : {deployment_id}")
292-
if entry is not None:
293-
for line in entry.format_summary():
294-
print(line)
279+
@chat_config.command(name="clear-token")
280+
@click.argument("deployment_id", type=click.UUID)
281+
def clear_token(deployment_id: UUID) -> None:
282+
"""Remove the stored API key for a deployment."""
283+
chat_config_store = load_chat_config()
284+
if chat_config_store.clear_token(deployment_id):
285+
save_chat_config(chat_config_store)
286+
print(f"Removed token for deployment {deployment_id}.")
295287
else:
296-
print("endpoint_url : -")
297-
print("default_model : -")
298-
print("last_synced_at: -")
299-
print(f"api_key : {mask_token(token)}")
288+
print(f"No token for deployment {deployment_id}.")
300289

301290

302291
__all__ = ("chat", "chat_config")

src/ai/backend/client/cli/v2/deployment/chat/types.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@ def format_summary(self) -> list[str]:
2424
f"last_synced_at: {self.last_synced_at.isoformat()}",
2525
]
2626

27+
@classmethod
28+
def print_summary(
29+
cls,
30+
deployment_id: UUID,
31+
entry: DeploymentChatCacheEntry | None,
32+
token_display: str,
33+
) -> None:
34+
print(f"deployment_id : {deployment_id}")
35+
if entry is not None:
36+
for line in entry.format_summary():
37+
print(line)
38+
else:
39+
print("endpoint_url : -")
40+
print("default_model : -")
41+
print("last_synced_at: -")
42+
print(f"api_key : {token_display}")
43+
2744

2845
class DeploymentChatCache(BaseModel):
2946
"""In-memory representation of the chat cache file."""

src/ai/backend/client/cli/v2/deployment/chat/utils.py

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,11 @@
1616
import tempfile
1717
from pathlib import Path
1818
from typing import Any
19-
from uuid import UUID
2019

2120
from pydantic import ValidationError
2221

2322
from ai.backend.client.cli.v2.deployment.chat.types import (
2423
DeploymentChatCache,
25-
DeploymentChatCacheEntry,
2624
DeploymentChatConfig,
2725
)
2826
from ai.backend.client.cli.v2.helpers import CONFIG_DIR
@@ -37,21 +35,10 @@ def load_chat_cache(path: Path = CHAT_CACHE_FILE) -> DeploymentChatCache:
3735
raw = _read_json(path)
3836
if raw is None:
3937
return DeploymentChatCache()
40-
deployments: dict[UUID, DeploymentChatCacheEntry] = {}
41-
deployments_raw = raw.get("deployments") or {}
42-
if isinstance(deployments_raw, dict):
43-
for key, value in deployments_raw.items():
44-
try:
45-
dep_id = UUID(str(key))
46-
except ValueError:
47-
continue
48-
if not isinstance(value, dict):
49-
continue
50-
try:
51-
deployments[dep_id] = DeploymentChatCacheEntry.model_validate(value)
52-
except ValidationError:
53-
continue
54-
return DeploymentChatCache(deployments=deployments)
38+
try:
39+
return DeploymentChatCache.model_validate(raw)
40+
except ValidationError:
41+
return DeploymentChatCache()
5542

5643

5744
def save_chat_cache(cache: DeploymentChatCache, path: Path = CHAT_CACHE_FILE) -> None:
@@ -64,17 +51,10 @@ def load_chat_config(path: Path = CHAT_CONFIG_FILE) -> DeploymentChatConfig:
6451
raw = _read_json(path)
6552
if raw is None:
6653
return DeploymentChatConfig()
67-
tokens: dict[UUID, str] = {}
68-
tokens_raw = raw.get("tokens") or {}
69-
if isinstance(tokens_raw, dict):
70-
for key, value in tokens_raw.items():
71-
try:
72-
dep_id = UUID(str(key))
73-
except ValueError:
74-
continue
75-
if isinstance(value, str):
76-
tokens[dep_id] = value
77-
return DeploymentChatConfig(tokens=tokens)
54+
try:
55+
return DeploymentChatConfig.model_validate(raw)
56+
except ValidationError:
57+
return DeploymentChatConfig()
7858

7959

8060
def save_chat_config(config: DeploymentChatConfig, path: Path = CHAT_CONFIG_FILE) -> None:

tests/unit/client/cli/test_deployment_chat_utils.py

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import stat
66
from datetime import UTC, datetime
77
from pathlib import Path
8-
from uuid import UUID, uuid4
8+
from uuid import uuid4
99

1010
import pytest
1111

@@ -100,9 +100,8 @@ def test_load_returns_empty_when_top_level_not_object(self, tmp_path: Path) -> N
100100
path.write_text("[]", encoding="utf-8")
101101
assert load_chat_cache(path).deployments == {}
102102

103-
def test_load_skips_invalid_uuid_keys(self, tmp_path: Path) -> None:
103+
def test_load_returns_empty_on_invalid_uuid_key(self, tmp_path: Path) -> None:
104104
path = tmp_path / "cache.json"
105-
good_id = UUID("12345678-1234-5678-1234-567812345678")
106105
path.write_text(
107106
json.dumps({
108107
"deployments": {
@@ -111,37 +110,23 @@ def test_load_skips_invalid_uuid_keys(self, tmp_path: Path) -> None:
111110
"default_model": None,
112111
"last_synced_at": "2026-04-27T12:00:00+00:00",
113112
},
114-
str(good_id): {
115-
"endpoint_url": "https://y.example",
116-
"default_model": "m",
117-
"last_synced_at": "2026-04-27T12:00:00+00:00",
118-
},
119113
},
120114
}),
121115
encoding="utf-8",
122116
)
123-
loaded = load_chat_cache(path)
124-
assert list(loaded.deployments.keys()) == [good_id]
117+
assert load_chat_cache(path).deployments == {}
125118

126-
def test_load_skips_malformed_entry_payload(self, tmp_path: Path) -> None:
119+
def test_load_returns_empty_on_malformed_entry_payload(self, tmp_path: Path) -> None:
127120
path = tmp_path / "cache.json"
128-
good_id = UUID("12345678-1234-5678-1234-567812345678")
129-
bad_id = UUID("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
130121
path.write_text(
131122
json.dumps({
132123
"deployments": {
133-
str(bad_id): {"default_model": "m"},
134-
str(good_id): {
135-
"endpoint_url": "https://y.example",
136-
"default_model": "m",
137-
"last_synced_at": "2026-04-27T12:00:00+00:00",
138-
},
124+
"12345678-1234-5678-1234-567812345678": {"default_model": "m"},
139125
},
140126
}),
141127
encoding="utf-8",
142128
)
143-
loaded = load_chat_cache(path)
144-
assert list(loaded.deployments.keys()) == [good_id]
129+
assert load_chat_cache(path).deployments == {}
145130

146131

147132
class TestConfigLoaderResilience:
@@ -150,20 +135,13 @@ def test_load_returns_empty_on_corrupt_json(self, tmp_path: Path) -> None:
150135
path.write_text("not-json{", encoding="utf-8")
151136
assert load_chat_config(path).tokens == {}
152137

153-
def test_load_skips_invalid_uuid_keys(self, tmp_path: Path) -> None:
138+
def test_load_returns_empty_on_invalid_uuid_key(self, tmp_path: Path) -> None:
154139
path = tmp_path / "config.json"
155-
good_id = UUID("12345678-1234-5678-1234-567812345678")
156140
path.write_text(
157-
json.dumps({
158-
"tokens": {
159-
"not-a-uuid": "sk-x",
160-
str(good_id): "sk-y",
161-
},
162-
}),
141+
json.dumps({"tokens": {"not-a-uuid": "sk-x"}}),
163142
encoding="utf-8",
164143
)
165-
loaded = load_chat_config(path)
166-
assert loaded.tokens == {good_id: "sk-y"}
144+
assert load_chat_config(path).tokens == {}
167145

168146

169147
class TestMaskToken:

0 commit comments

Comments
 (0)