softmax-cli 0.26.27__tar.gz → 0.26.30__tar.gz
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.
- {softmax_cli-0.26.27/src/softmax_cli.egg-info → softmax_cli-0.26.30}/PKG-INFO +1 -1
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax/auth.py +34 -43
- {softmax_cli-0.26.27 → softmax_cli-0.26.30/src/softmax_cli.egg-info}/PKG-INFO +1 -1
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/tests/test_python_api.py +62 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/AGENTS.md +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/BUILD.bazel +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/CLAUDE.md +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/README.md +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/pyproject.toml +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/setup.cfg +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax/__init__.py +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax/_console.py +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax/cli.py +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax/perform_login.py +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax/players.py +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax_cli.egg-info/SOURCES.txt +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax_cli.egg-info/dependency_links.txt +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax_cli.egg-info/entry_points.txt +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax_cli.egg-info/requires.txt +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/src/softmax_cli.egg-info/top_level.txt +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/tests/BUILD.bazel +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/tests/test_auth_login.py +0 -0
- {softmax_cli-0.26.27 → softmax_cli-0.26.30}/tests/test_player_cli.py +0 -0
|
@@ -9,7 +9,7 @@ from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
import yaml
|
|
12
|
-
from pydantic import BaseModel, Field
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
13
|
|
|
14
14
|
DEFAULT_API_SERVER = "https://softmax.com/api"
|
|
15
15
|
|
|
@@ -44,6 +44,23 @@ def _is_trusted_exchange_host(api_server: str) -> bool:
|
|
|
44
44
|
# --- on-disk storage ---
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
class CachedPlayerSession(BaseModel):
|
|
48
|
+
token: str
|
|
49
|
+
expires_at: datetime
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ServerPlayerSessions(BaseModel):
|
|
53
|
+
active: str | None = None
|
|
54
|
+
cache: dict[str, CachedPlayerSession] = Field(default_factory=dict)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Credentials(BaseModel):
|
|
58
|
+
model_config = ConfigDict(extra="allow")
|
|
59
|
+
|
|
60
|
+
tokens: dict[str, str] = Field(default_factory=dict)
|
|
61
|
+
player_sessions: dict[str, ServerPlayerSessions] = Field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
|
|
47
64
|
def _config_dir() -> Path:
|
|
48
65
|
config_dir = Path.home() / ".softmax"
|
|
49
66
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -54,19 +71,19 @@ def _credentials_path() -> Path:
|
|
|
54
71
|
return _config_dir() / CREDENTIALS_FILE
|
|
55
72
|
|
|
56
73
|
|
|
57
|
-
def _load_data() ->
|
|
74
|
+
def _load_data() -> Credentials:
|
|
58
75
|
cred_path = _credentials_path()
|
|
59
76
|
if not cred_path.exists():
|
|
60
|
-
return
|
|
77
|
+
return Credentials()
|
|
61
78
|
|
|
62
79
|
with open(cred_path, "r") as f:
|
|
63
|
-
return yaml.safe_load(f) or {
|
|
80
|
+
return Credentials.model_validate(yaml.safe_load(f) or {})
|
|
64
81
|
|
|
65
82
|
|
|
66
|
-
def _save_data(data:
|
|
83
|
+
def _save_data(data: Credentials) -> None:
|
|
67
84
|
cred_path = _credentials_path()
|
|
68
85
|
with open(cred_path, "w") as f:
|
|
69
|
-
yaml.safe_dump(data, f, default_flow_style=False)
|
|
86
|
+
yaml.safe_dump(data.model_dump(mode="json"), f, default_flow_style=False)
|
|
70
87
|
os.chmod(cred_path, 0o600)
|
|
71
88
|
|
|
72
89
|
|
|
@@ -75,34 +92,20 @@ def _save_data(data: dict) -> None:
|
|
|
75
92
|
|
|
76
93
|
def load_user_token(*, server: str) -> str | None:
|
|
77
94
|
server = server.rstrip("/")
|
|
78
|
-
|
|
79
|
-
token = tokens.get(server)
|
|
80
|
-
return token if isinstance(token, str) else None
|
|
95
|
+
return _load_data().tokens.get(server)
|
|
81
96
|
|
|
82
97
|
|
|
83
98
|
def save_user_token(*, server: str, token: str) -> None:
|
|
84
99
|
server = server.rstrip("/")
|
|
85
100
|
data = _load_data()
|
|
86
|
-
tokens = data.setdefault("tokens", {})
|
|
87
101
|
# Player sessions are owned by whoever held the user token. When the user
|
|
88
102
|
# credential changes (account switch, `login --force`, CI token rotation),
|
|
89
103
|
# the old player sessions belong to the previous user, so drop them rather
|
|
90
104
|
# than letting load_current_token keep acting as that stale player.
|
|
91
|
-
if tokens.get(server) != token:
|
|
92
|
-
data.
|
|
93
|
-
tokens[server] = token
|
|
94
|
-
_save_data(data)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _delete_user_token(*, server: str) -> bool:
|
|
98
|
-
server = server.rstrip("/")
|
|
99
|
-
data = _load_data()
|
|
100
|
-
tokens = data.get("tokens", {})
|
|
101
|
-
if server not in tokens:
|
|
102
|
-
return False
|
|
103
|
-
del tokens[server]
|
|
105
|
+
if data.tokens.get(server) != token:
|
|
106
|
+
data.player_sessions.pop(server, None)
|
|
107
|
+
data.tokens[server] = token
|
|
104
108
|
_save_data(data)
|
|
105
|
-
return True
|
|
106
109
|
|
|
107
110
|
|
|
108
111
|
# --- player sessions ---
|
|
@@ -119,23 +122,12 @@ def _delete_user_token(*, server: str) -> bool:
|
|
|
119
122
|
# ply_aaa: {token: ply_..., expires_at: "2026-..."}
|
|
120
123
|
|
|
121
124
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
expires_at: datetime
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class ServerPlayerSessions(BaseModel):
|
|
128
|
-
active: str | None = None
|
|
129
|
-
cache: dict[str, CachedPlayerSession] = Field(default_factory=dict)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _server_sessions(data: dict, server: str) -> ServerPlayerSessions:
|
|
133
|
-
raw = (data.get("player_sessions") or {}).get(server.rstrip("/")) or {}
|
|
134
|
-
return ServerPlayerSessions.model_validate(raw)
|
|
125
|
+
def _server_sessions(data: Credentials, server: str) -> ServerPlayerSessions:
|
|
126
|
+
return data.player_sessions.get(server.rstrip("/"), ServerPlayerSessions())
|
|
135
127
|
|
|
136
128
|
|
|
137
|
-
def _store_server_sessions(data:
|
|
138
|
-
data.
|
|
129
|
+
def _store_server_sessions(data: Credentials, server: str, sessions: ServerPlayerSessions) -> None:
|
|
130
|
+
data.player_sessions[server.rstrip("/")] = sessions
|
|
139
131
|
|
|
140
132
|
|
|
141
133
|
def load_player_session(*, server: str) -> str | None:
|
|
@@ -199,11 +191,10 @@ def has_saved_token(*, server: str) -> bool:
|
|
|
199
191
|
|
|
200
192
|
def delete_all_tokens(*, server: str) -> bool:
|
|
201
193
|
server = server.rstrip("/")
|
|
202
|
-
deleted_token = _delete_user_token(server=server)
|
|
203
194
|
data = _load_data()
|
|
204
|
-
|
|
205
|
-
deleted_session =
|
|
206
|
-
if deleted_session:
|
|
195
|
+
deleted_token = data.tokens.pop(server, None) is not None
|
|
196
|
+
deleted_session = data.player_sessions.pop(server, None) is not None
|
|
197
|
+
if deleted_token or deleted_session:
|
|
207
198
|
_save_data(data)
|
|
208
199
|
return deleted_token or deleted_session
|
|
209
200
|
|
|
@@ -4,8 +4,11 @@ import sys
|
|
|
4
4
|
from datetime import UTC, datetime, timedelta
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
+
import yaml
|
|
8
|
+
from pydantic import ValidationError
|
|
7
9
|
|
|
8
10
|
import softmax
|
|
11
|
+
import softmax.auth as auth
|
|
9
12
|
from softmax.auth import (
|
|
10
13
|
clear_active_player_session,
|
|
11
14
|
get_cached_player_session,
|
|
@@ -26,6 +29,65 @@ def _activate_player(server: str, token: str, player_id: str = "ply_alpha") -> N
|
|
|
26
29
|
)
|
|
27
30
|
|
|
28
31
|
|
|
32
|
+
@pytest.mark.parametrize(
|
|
33
|
+
"credentials",
|
|
34
|
+
[
|
|
35
|
+
{"tokens": {"https://softmax.com/api": 123}},
|
|
36
|
+
{
|
|
37
|
+
"player_sessions": {
|
|
38
|
+
"https://softmax.com/api": {"active": ["ply_alpha"], "cache": {}},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
def test_malformed_credentials_fail_validation(monkeypatch: pytest.MonkeyPatch, tmp_path, credentials) -> None:
|
|
44
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
45
|
+
config_dir = tmp_path / ".softmax"
|
|
46
|
+
config_dir.mkdir()
|
|
47
|
+
(config_dir / "credentials.yaml").write_text(yaml.safe_dump(credentials))
|
|
48
|
+
|
|
49
|
+
with pytest.raises(ValidationError):
|
|
50
|
+
auth._load_data()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_unknown_credential_fields_survive_save(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
|
|
54
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
55
|
+
config_dir = tmp_path / ".softmax"
|
|
56
|
+
config_dir.mkdir()
|
|
57
|
+
credentials_path = config_dir / "credentials.yaml"
|
|
58
|
+
credentials_path.write_text(yaml.safe_dump({"credential_version": {"major": 2}}))
|
|
59
|
+
|
|
60
|
+
save_user_token(server="https://softmax.com/api", token="user-token")
|
|
61
|
+
|
|
62
|
+
saved = yaml.safe_load(credentials_path.read_text())
|
|
63
|
+
assert saved["credential_version"] == {"major": 2}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_delete_all_tokens_is_one_credentials_update(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
|
|
67
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
68
|
+
server = "https://softmax.com/api"
|
|
69
|
+
save_user_token(server=server, token="user-token")
|
|
70
|
+
_activate_player(server, "player-token")
|
|
71
|
+
credentials = auth._load_data()
|
|
72
|
+
calls = {"loads": 0, "saves": 0}
|
|
73
|
+
|
|
74
|
+
def load() -> auth.Credentials:
|
|
75
|
+
calls["loads"] += 1
|
|
76
|
+
return credentials
|
|
77
|
+
|
|
78
|
+
def save(data: auth.Credentials) -> None:
|
|
79
|
+
calls["saves"] += 1
|
|
80
|
+
assert data is credentials
|
|
81
|
+
|
|
82
|
+
monkeypatch.setattr(auth, "_load_data", load)
|
|
83
|
+
monkeypatch.setattr(auth, "_save_data", save)
|
|
84
|
+
|
|
85
|
+
assert auth.delete_all_tokens(server=server) is True
|
|
86
|
+
assert calls == {"loads": 1, "saves": 1}
|
|
87
|
+
assert server not in credentials.tokens
|
|
88
|
+
assert server not in credentials.player_sessions
|
|
89
|
+
|
|
90
|
+
|
|
29
91
|
def test_login_returns_saved_token(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
|
|
30
92
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
31
93
|
save_user_token(server="https://softmax.com/api", token="saved-token")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|