softmax-cli 0.26.7__tar.gz → 0.26.9__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.7/src/softmax_cli.egg-info → softmax_cli-0.26.9}/PKG-INFO +1 -1
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax/auth.py +5 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax/cli.py +62 -43
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax/cogames.py +6 -9
- {softmax_cli-0.26.7 → softmax_cli-0.26.9/src/softmax_cli.egg-info}/PKG-INFO +1 -1
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/tests/BUILD.bazel +6 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/tests/test_auth_login.py +2 -6
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/tests/test_python_api.py +3 -7
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/BUILD.bazel +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/pyproject.toml +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/setup.cfg +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax/__init__.py +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax/_console.py +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax/perform_login.py +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax/token_storage.py +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax_cli.egg-info/SOURCES.txt +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax_cli.egg-info/dependency_links.txt +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax_cli.egg-info/entry_points.txt +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax_cli.egg-info/requires.txt +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/src/softmax_cli.egg-info/top_level.txt +0 -0
- {softmax_cli-0.26.7 → softmax_cli-0.26.9}/tests/test_cli_plugins.py +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
@@ -16,6 +17,10 @@ DEFAULT_COGAMES_SERVER = "https://softmax.com/api"
|
|
|
16
17
|
DEFAULT_COGAMES_API_SERVER = "https://api.observatory.softmax-research.net"
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def get_login_server() -> str:
|
|
21
|
+
return os.environ.get("COGAMES_LOGIN_URL", DEFAULT_COGAMES_SERVER)
|
|
22
|
+
|
|
23
|
+
|
|
19
24
|
class WhoAmIResponse(BaseModel):
|
|
20
25
|
user_email: str
|
|
21
26
|
is_softmax_team_member: bool = False
|
|
@@ -4,6 +4,7 @@ import importlib
|
|
|
4
4
|
import importlib.util
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
+
import httpx
|
|
7
8
|
import typer
|
|
8
9
|
from rich.panel import Panel
|
|
9
10
|
|
|
@@ -13,6 +14,7 @@ from softmax.auth import (
|
|
|
13
14
|
build_browser_login_url,
|
|
14
15
|
delete_cogames_tokens,
|
|
15
16
|
fetch_cogames_whoami,
|
|
17
|
+
get_login_server,
|
|
16
18
|
load_cogames_user_token,
|
|
17
19
|
load_current_cogames_token,
|
|
18
20
|
save_cogames_active_token,
|
|
@@ -39,15 +41,16 @@ def _register_optional_apps() -> None:
|
|
|
39
41
|
app.add_typer(cogames_cli.app, name="cogames", rich_help_panel="Local Games")
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def _build_manual_set_token_command(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return command
|
|
44
|
+
def _build_manual_set_token_command(server: str | None = None) -> str:
|
|
45
|
+
if server:
|
|
46
|
+
return f"softmax set-token --server '{server}' '<TOKEN>'"
|
|
47
|
+
return "softmax set-token '<TOKEN>'"
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def _print_non_tty_login_instructions(
|
|
50
|
-
|
|
50
|
+
def _print_non_tty_login_instructions(login_server: str | None = None) -> None:
|
|
51
|
+
resolved = login_server or get_login_server()
|
|
52
|
+
is_custom = resolved != DEFAULT_COGAMES_SERVER
|
|
53
|
+
auth_url = build_browser_login_url(resolved)
|
|
51
54
|
console.print("Interactive login requires a TTY.", style="red")
|
|
52
55
|
console.print()
|
|
53
56
|
console.print("Open this URL in any browser to sign in:", style="yellow")
|
|
@@ -56,7 +59,7 @@ def _print_non_tty_login_instructions(*, login_server: str) -> None:
|
|
|
56
59
|
console.print()
|
|
57
60
|
console.print("Copy the auth token from the browser, then run:", style="yellow")
|
|
58
61
|
console.print()
|
|
59
|
-
console.print(" ", _build_manual_set_token_command(
|
|
62
|
+
console.print(" ", _build_manual_set_token_command(resolved if is_custom else None))
|
|
60
63
|
console.print()
|
|
61
64
|
console.print(
|
|
62
65
|
Panel(
|
|
@@ -70,12 +73,6 @@ def _print_non_tty_login_instructions(*, login_server: str) -> None:
|
|
|
70
73
|
|
|
71
74
|
@app.command(name="login")
|
|
72
75
|
def login_cmd(
|
|
73
|
-
login_server: str = typer.Option(
|
|
74
|
-
DEFAULT_COGAMES_SERVER,
|
|
75
|
-
"--login-server",
|
|
76
|
-
metavar="URL",
|
|
77
|
-
help="Authentication server URL",
|
|
78
|
-
),
|
|
79
76
|
no_browser: bool = typer.Option(
|
|
80
77
|
False,
|
|
81
78
|
"--no-browser",
|
|
@@ -87,18 +84,40 @@ def login_cmd(
|
|
|
87
84
|
"-f",
|
|
88
85
|
help="Re-authenticate even if already logged in",
|
|
89
86
|
),
|
|
87
|
+
server: str | None = typer.Option(
|
|
88
|
+
None,
|
|
89
|
+
"--server",
|
|
90
|
+
"-s",
|
|
91
|
+
metavar="URL",
|
|
92
|
+
help="Authentication server URL.",
|
|
93
|
+
),
|
|
90
94
|
) -> None:
|
|
91
95
|
"""Sign in to Softmax."""
|
|
92
96
|
from urllib.parse import urlparse # noqa: PLC0415
|
|
93
97
|
|
|
98
|
+
login_server = server or get_login_server()
|
|
99
|
+
is_default_server = login_server == DEFAULT_COGAMES_SERVER
|
|
94
100
|
user_token = None if force else load_cogames_user_token(login_server=login_server)
|
|
101
|
+
if user_token is not None:
|
|
102
|
+
api_server = None if is_default_server else login_server
|
|
103
|
+
try:
|
|
104
|
+
fetch_cogames_whoami(api_server=api_server, token=user_token)
|
|
105
|
+
except httpx.HTTPStatusError as exc:
|
|
106
|
+
if exc.response.status_code == 401:
|
|
107
|
+
console.print("Saved token is no longer valid. Re-authenticating...", style="yellow")
|
|
108
|
+
user_token = None
|
|
109
|
+
else:
|
|
110
|
+
console.print(f"Could not verify token (HTTP {exc.response.status_code}), proceeding.", style="yellow")
|
|
111
|
+
except httpx.HTTPError:
|
|
112
|
+
console.print("Could not reach server to verify token. Proceeding with saved token.", style="yellow")
|
|
113
|
+
|
|
95
114
|
if user_token is not None:
|
|
96
115
|
save_cogames_active_token(login_server=login_server, token=user_token)
|
|
97
116
|
console.print(f"Already authenticated with {urlparse(login_server).hostname}", style="green")
|
|
98
117
|
return
|
|
99
118
|
|
|
100
119
|
if not sys.stdin.isatty():
|
|
101
|
-
_print_non_tty_login_instructions(login_server
|
|
120
|
+
_print_non_tty_login_instructions(login_server)
|
|
102
121
|
raise typer.Exit(1)
|
|
103
122
|
|
|
104
123
|
try:
|
|
@@ -110,7 +129,7 @@ def login_cmd(
|
|
|
110
129
|
"If you are a coding agent, ask your human to open the URL below and give you "
|
|
111
130
|
"the auth token. Then paste the token into this window or run:\n"
|
|
112
131
|
"\n"
|
|
113
|
-
f"{_build_manual_set_token_command(
|
|
132
|
+
f"{_build_manual_set_token_command(server)}"
|
|
114
133
|
),
|
|
115
134
|
open_browser=not no_browser,
|
|
116
135
|
)
|
|
@@ -128,14 +147,16 @@ def login_cmd(
|
|
|
128
147
|
|
|
129
148
|
@app.command(name="logout")
|
|
130
149
|
def logout_cmd(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"--
|
|
150
|
+
server: str | None = typer.Option(
|
|
151
|
+
None,
|
|
152
|
+
"--server",
|
|
153
|
+
"-s",
|
|
134
154
|
metavar="URL",
|
|
135
|
-
help="Authentication server URL",
|
|
155
|
+
help="Authentication server URL.",
|
|
136
156
|
),
|
|
137
157
|
) -> None:
|
|
138
158
|
"""Remove saved authentication token."""
|
|
159
|
+
login_server = server or get_login_server()
|
|
139
160
|
if delete_cogames_tokens(login_server=login_server):
|
|
140
161
|
console.print("Logged out.", style="green")
|
|
141
162
|
else:
|
|
@@ -144,42 +165,36 @@ def logout_cmd(
|
|
|
144
165
|
|
|
145
166
|
@app.command(name="get-login-url")
|
|
146
167
|
def get_login_url_cmd(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"--
|
|
168
|
+
server: str | None = typer.Option(
|
|
169
|
+
None,
|
|
170
|
+
"--server",
|
|
171
|
+
"-s",
|
|
150
172
|
metavar="URL",
|
|
151
|
-
help="Authentication server URL",
|
|
173
|
+
help="Authentication server URL.",
|
|
152
174
|
),
|
|
153
175
|
) -> None:
|
|
154
176
|
"""Print a browser sign-in URL for manual login."""
|
|
155
|
-
print(build_browser_login_url(
|
|
177
|
+
print(build_browser_login_url(server or get_login_server()))
|
|
156
178
|
|
|
157
179
|
|
|
158
180
|
@app.command(name="status")
|
|
159
181
|
def status_cmd(
|
|
160
|
-
login_server: str = typer.Option(
|
|
161
|
-
DEFAULT_COGAMES_SERVER,
|
|
162
|
-
"--login-server",
|
|
163
|
-
metavar="URL",
|
|
164
|
-
help="Authentication server URL",
|
|
165
|
-
),
|
|
166
182
|
server: str | None = typer.Option(
|
|
167
183
|
None,
|
|
168
184
|
"--server",
|
|
169
185
|
"-s",
|
|
170
186
|
metavar="URL",
|
|
171
|
-
help="
|
|
172
|
-
" that is overridden, otherwise the production Observatory API.",
|
|
187
|
+
help="Authentication server URL.",
|
|
173
188
|
),
|
|
174
189
|
) -> None:
|
|
175
190
|
"""Check authentication status via /whoami."""
|
|
191
|
+
login_server = server or get_login_server()
|
|
176
192
|
token = load_current_cogames_token(login_server=login_server)
|
|
177
193
|
if not token:
|
|
178
194
|
console.print("[red]Not authenticated.[/red] Run [cyan]softmax login[/cyan] first.")
|
|
179
195
|
raise typer.Exit(1)
|
|
180
196
|
|
|
181
|
-
|
|
182
|
-
session = fetch_cogames_whoami(api_server=api_server, token=token)
|
|
197
|
+
session = fetch_cogames_whoami(api_server=server, token=token)
|
|
183
198
|
console.print("[green]Authenticated[/green]")
|
|
184
199
|
console.print(f"user_email: {session.user_email}")
|
|
185
200
|
console.print(f"subject_type: {session.subject_type}")
|
|
@@ -189,14 +204,16 @@ def status_cmd(
|
|
|
189
204
|
|
|
190
205
|
@app.command(name="get-token")
|
|
191
206
|
def get_token_cmd(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"--
|
|
207
|
+
server: str | None = typer.Option(
|
|
208
|
+
None,
|
|
209
|
+
"--server",
|
|
210
|
+
"-s",
|
|
195
211
|
metavar="URL",
|
|
196
|
-
help="Authentication server URL",
|
|
212
|
+
help="Authentication server URL.",
|
|
197
213
|
),
|
|
198
214
|
) -> None:
|
|
199
215
|
"""Print the saved token to stdout (for scripting)."""
|
|
216
|
+
login_server = server or get_login_server()
|
|
200
217
|
token = load_current_cogames_token(login_server=login_server)
|
|
201
218
|
if not token:
|
|
202
219
|
console.print("[red]No token found.[/red] Run [cyan]softmax login[/cyan] first.", style="bold")
|
|
@@ -207,14 +224,16 @@ def get_token_cmd(
|
|
|
207
224
|
@app.command(name="set-token")
|
|
208
225
|
def set_token_cmd(
|
|
209
226
|
token: str = typer.Argument(help="Bearer token to save"),
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"--
|
|
227
|
+
server: str | None = typer.Option(
|
|
228
|
+
None,
|
|
229
|
+
"--server",
|
|
230
|
+
"-s",
|
|
213
231
|
metavar="URL",
|
|
214
|
-
help="Authentication server URL",
|
|
232
|
+
help="Authentication server URL.",
|
|
215
233
|
),
|
|
216
234
|
) -> None:
|
|
217
235
|
"""Manually set a token (for CI or headless environments)."""
|
|
236
|
+
login_server = server or get_login_server()
|
|
218
237
|
save_cogames_user_token(login_server=login_server, token=token)
|
|
219
238
|
print(f"\nToken saved for {login_server}")
|
|
220
239
|
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import importlib
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from softmax.auth import DEFAULT_COGAMES_API_SERVER
|
|
6
|
+
from softmax.auth import DEFAULT_COGAMES_API_SERVER
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _get_tournament_client_class() -> type[Any]:
|
|
@@ -16,9 +16,9 @@ def _get_tournament_client_class() -> type[Any]:
|
|
|
16
16
|
return tournament_client_module.TournamentServerClient
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def _create_client(*, token: str, server: str
|
|
19
|
+
def _create_client(*, token: str, server: str) -> Any:
|
|
20
20
|
client_class = _get_tournament_client_class()
|
|
21
|
-
return client_class(server_url=server, token=token
|
|
21
|
+
return client_class(server_url=server, token=token)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class _PlayerAPI:
|
|
@@ -27,9 +27,8 @@ class _PlayerAPI:
|
|
|
27
27
|
token: str,
|
|
28
28
|
*,
|
|
29
29
|
server: str = DEFAULT_COGAMES_API_SERVER,
|
|
30
|
-
login_server: str = DEFAULT_COGAMES_SERVER,
|
|
31
30
|
) -> list[Any]:
|
|
32
|
-
with _create_client(token=token, server=server
|
|
31
|
+
with _create_client(token=token, server=server) as client:
|
|
33
32
|
return client.list_players()
|
|
34
33
|
|
|
35
34
|
|
|
@@ -41,9 +40,8 @@ def login(
|
|
|
41
40
|
player_id: str,
|
|
42
41
|
*,
|
|
43
42
|
server: str = DEFAULT_COGAMES_API_SERVER,
|
|
44
|
-
login_server: str = DEFAULT_COGAMES_SERVER,
|
|
45
43
|
) -> str:
|
|
46
|
-
with _create_client(token=token, server=server
|
|
44
|
+
with _create_client(token=token, server=server) as client:
|
|
47
45
|
return client.login_player(player_id).token
|
|
48
46
|
|
|
49
47
|
|
|
@@ -52,9 +50,8 @@ def login_response(
|
|
|
52
50
|
player_id: str,
|
|
53
51
|
*,
|
|
54
52
|
server: str = DEFAULT_COGAMES_API_SERVER,
|
|
55
|
-
login_server: str = DEFAULT_COGAMES_SERVER,
|
|
56
53
|
) -> Any:
|
|
57
|
-
with _create_client(token=token, server=server
|
|
54
|
+
with _create_client(token=token, server=server) as client:
|
|
58
55
|
return client.login_player(player_id)
|
|
59
56
|
|
|
60
57
|
|
|
@@ -25,6 +25,12 @@ _TEST_FILES = glob(
|
|
|
25
25
|
],
|
|
26
26
|
deps = [
|
|
27
27
|
"//packages/softmax-cli:softmax",
|
|
28
|
+
# test_cli_plugins exercises Typer entry-point discovery — it
|
|
29
|
+
# asserts `cogames` registers as a softmax subcommand. With
|
|
30
|
+
# per-package @pyenv targets only deps you name reach the
|
|
31
|
+
# runfiles tree, so the cogames dist-info has to be pulled in
|
|
32
|
+
# explicitly for the entry-point loader to find it.
|
|
33
|
+
"@pyenv//:cogames",
|
|
28
34
|
"@pyenv//:pytest",
|
|
29
35
|
],
|
|
30
36
|
)
|
|
@@ -146,12 +146,8 @@ def test_authenticate_reprompts_after_invalid_token(
|
|
|
146
146
|
assert "Invalid token. Please try again." in capsys.readouterr().out
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
def
|
|
150
|
-
assert _build_manual_set_token_command(
|
|
151
|
-
assert (
|
|
152
|
-
_build_manual_set_token_command(login_server="https://example.ngrok.app/api")
|
|
153
|
-
== "softmax set-token '<TOKEN>' --login-server 'https://example.ngrok.app/api'"
|
|
154
|
-
)
|
|
149
|
+
def test_manual_command_format() -> None:
|
|
150
|
+
assert _build_manual_set_token_command() == "softmax set-token '<TOKEN>'"
|
|
155
151
|
|
|
156
152
|
|
|
157
153
|
def test_generic_authenticator_does_not_print_cogames_agent_hint(
|
|
@@ -74,10 +74,9 @@ def test_softmax_cogames_player_list_uses_expected_defaults(monkeypatch: pytest.
|
|
|
74
74
|
captured: dict[str, object] = {}
|
|
75
75
|
|
|
76
76
|
class FakeClient:
|
|
77
|
-
def __init__(self, *, server_url: str, token: str
|
|
77
|
+
def __init__(self, *, server_url: str, token: str) -> None:
|
|
78
78
|
captured["server_url"] = server_url
|
|
79
79
|
captured["token"] = token
|
|
80
|
-
captured["login_server"] = login_server
|
|
81
80
|
|
|
82
81
|
def __enter__(self) -> "FakeClient":
|
|
83
82
|
return self
|
|
@@ -94,7 +93,6 @@ def test_softmax_cogames_player_list_uses_expected_defaults(monkeypatch: pytest.
|
|
|
94
93
|
assert captured == {
|
|
95
94
|
"server_url": "https://api.observatory.softmax-research.net",
|
|
96
95
|
"token": "softmax-token",
|
|
97
|
-
"login_server": "https://softmax.com/api",
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
|
|
@@ -103,10 +101,9 @@ def test_softmax_cogames_login_returns_player_token(monkeypatch: pytest.MonkeyPa
|
|
|
103
101
|
token = "player-token"
|
|
104
102
|
|
|
105
103
|
class FakeClient:
|
|
106
|
-
def __init__(self, *, server_url: str, token: str
|
|
104
|
+
def __init__(self, *, server_url: str, token: str) -> None:
|
|
107
105
|
self.server_url = server_url
|
|
108
106
|
self.token = token
|
|
109
|
-
self.login_server = login_server
|
|
110
107
|
|
|
111
108
|
def __enter__(self) -> "FakeClient":
|
|
112
109
|
return self
|
|
@@ -117,7 +114,6 @@ def test_softmax_cogames_login_returns_player_token(monkeypatch: pytest.MonkeyPa
|
|
|
117
114
|
def login_player(self, player_id: str) -> FakeLoginResponse:
|
|
118
115
|
assert self.server_url == "https://api.observatory.softmax-research.net"
|
|
119
116
|
assert self.token == "softmax-token"
|
|
120
|
-
assert self.login_server == "https://softmax.com/api"
|
|
121
117
|
assert player_id == "ply_alpha"
|
|
122
118
|
return FakeLoginResponse()
|
|
123
119
|
|
|
@@ -132,7 +128,7 @@ def test_softmax_cogames_login_response_returns_full_response(monkeypatch: pytes
|
|
|
132
128
|
expires_at = "2026-02-21T12:00:00Z"
|
|
133
129
|
|
|
134
130
|
class FakeClient:
|
|
135
|
-
def __init__(self, *, server_url: str, token: str
|
|
131
|
+
def __init__(self, *, server_url: str, token: str) -> None:
|
|
136
132
|
pass
|
|
137
133
|
|
|
138
134
|
def __enter__(self) -> "FakeClient":
|
|
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
|