sweatstack-cli 0.8.0__tar.gz → 0.9.0__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.
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/PKG-INFO +2 -2
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/README.md +1 -1
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/pyproject.toml +1 -1
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/__init__.py +1 -1
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/commands/app.py +33 -2
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_commands/test_app.py +104 -1
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/settings.local.json +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/SKILL.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/api.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/auth.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/cli.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/pyscript.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/webapp.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.gitignore +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.python-version +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/CREATE_PRIVATE_APP.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/DEVELOPMENT.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/Makefile +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/PLAN.md +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/api/__init__.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/api/client.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/__init__.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/callback_server.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/jwt.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/pkce.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/tokens.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/commands/__init__.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/commands/auth.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/commands/page.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/config.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/console.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/exceptions.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/main.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/project.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/py.typed +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/version_check.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/sweatstack.toml +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/__init__.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/conftest.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_api/__init__.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_api/test_client.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/__init__.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/test_callback_server.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/test_jwt.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/test_pkce.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/test_tokens.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_commands/__init__.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_commands/test_cli.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_project.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_version_check.py +0 -0
- {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sweatstack-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Command-line interface for SweatStack — the sports data platform for developers
|
|
5
5
|
Project-URL: Homepage, https://sweatstack.no
|
|
6
6
|
Project-URL: Documentation, https://docs.sweatstack.no
|
|
@@ -88,7 +88,7 @@ sweatstack page deploy
|
|
|
88
88
|
|
|
89
89
|
| Command | Description |
|
|
90
90
|
|---------|-------------|
|
|
91
|
-
| `sweatstack app create NAME` | Create a new private OAuth2 application |
|
|
91
|
+
| `sweatstack app create [NAME]` | Create a new private OAuth2 application |
|
|
92
92
|
| `sweatstack app link` | Link current directory to an existing application |
|
|
93
93
|
|
|
94
94
|
#### `app create` options
|
|
@@ -58,7 +58,7 @@ sweatstack page deploy
|
|
|
58
58
|
|
|
59
59
|
| Command | Description |
|
|
60
60
|
|---------|-------------|
|
|
61
|
-
| `sweatstack app create NAME` | Create a new private OAuth2 application |
|
|
61
|
+
| `sweatstack app create [NAME]` | Create a new private OAuth2 application |
|
|
62
62
|
| `sweatstack app link` | Link current directory to an existing application |
|
|
63
63
|
|
|
64
64
|
#### `app create` options
|
|
@@ -17,9 +17,14 @@ from sweatstack_cli.project import CONFIG_FILENAME, write_config
|
|
|
17
17
|
app = typer.Typer(name="app", help="Manage OAuth2 applications.")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _is_interactive() -> bool:
|
|
21
|
+
"""Check if stdin is a TTY (interactive terminal)."""
|
|
22
|
+
return sys.stdin.isatty()
|
|
23
|
+
|
|
24
|
+
|
|
20
25
|
@app.command()
|
|
21
26
|
def create(
|
|
22
|
-
name: str = typer.Argument(
|
|
27
|
+
name: str | None = typer.Argument(None, help="Application name."),
|
|
23
28
|
description: str | None = typer.Option(
|
|
24
29
|
None,
|
|
25
30
|
"--description",
|
|
@@ -59,6 +64,32 @@ def create(
|
|
|
59
64
|
console.print("[red]Error:[/red] --env and --env-file are mutually exclusive.")
|
|
60
65
|
raise typer.Exit(1)
|
|
61
66
|
|
|
67
|
+
interactive = _is_interactive()
|
|
68
|
+
|
|
69
|
+
# Resolve name
|
|
70
|
+
if name is None:
|
|
71
|
+
if not interactive:
|
|
72
|
+
console.print("[red]Error:[/red] App name is required.")
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
name = typer.prompt("App name")
|
|
75
|
+
|
|
76
|
+
# Prompt for optional fields in interactive mode
|
|
77
|
+
if interactive:
|
|
78
|
+
if description is None:
|
|
79
|
+
description = typer.prompt(
|
|
80
|
+
"Description (optional)", default="", show_default=False
|
|
81
|
+
)
|
|
82
|
+
if not description:
|
|
83
|
+
description = None
|
|
84
|
+
if page is None:
|
|
85
|
+
page = typer.prompt(
|
|
86
|
+
"Page slug (optional)", default="", show_default=False
|
|
87
|
+
)
|
|
88
|
+
if not page:
|
|
89
|
+
page = None
|
|
90
|
+
if not secret:
|
|
91
|
+
secret = typer.confirm("Generate client secret?", default=False)
|
|
92
|
+
|
|
62
93
|
body: dict[str, str | bool] = {"name": name}
|
|
63
94
|
|
|
64
95
|
if description:
|
|
@@ -90,7 +121,7 @@ def create(
|
|
|
90
121
|
# Write sweatstack.toml (protect existing file)
|
|
91
122
|
config_path = Path(CONFIG_FILENAME)
|
|
92
123
|
if config_path.exists():
|
|
93
|
-
if not
|
|
124
|
+
if not interactive:
|
|
94
125
|
console.print(
|
|
95
126
|
f"[red]Error:[/red] {CONFIG_FILENAME} already exists. "
|
|
96
127
|
"Remove it first or run interactively."
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from unittest.mock import MagicMock, patch
|
|
6
|
+
from unittest.mock import MagicMock, PropertyMock, patch
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
from typer.testing import CliRunner
|
|
@@ -247,3 +247,106 @@ class TestAppCreate:
|
|
|
247
247
|
assert result.exit_code == 0
|
|
248
248
|
assert "won't be shown again" not in result.stdout
|
|
249
249
|
assert "written to" in result.stdout
|
|
250
|
+
|
|
251
|
+
def test_create_noninteractive_requires_name(self) -> None:
|
|
252
|
+
"""Should error when name is missing in non-interactive mode."""
|
|
253
|
+
result = runner.invoke(app, ["app", "create"])
|
|
254
|
+
|
|
255
|
+
assert result.exit_code == 1
|
|
256
|
+
assert "App name is required" in result.stdout
|
|
257
|
+
|
|
258
|
+
def test_create_interactive_full(self, mock_api: MagicMock, tmp_path: Path) -> None:
|
|
259
|
+
"""Should prompt for all fields when no args given interactively."""
|
|
260
|
+
mock_api.post.return_value = {"name": "My App", "id": "abc123"}
|
|
261
|
+
|
|
262
|
+
with patch("sweatstack_cli.commands.app._is_interactive", return_value=True):
|
|
263
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
264
|
+
result = runner.invoke(
|
|
265
|
+
app, ["app", "create"], input="My App\n\n\nn\n"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
assert result.exit_code == 0
|
|
269
|
+
assert "My App" in result.stdout
|
|
270
|
+
mock_api.post.assert_called_once_with(
|
|
271
|
+
"/api/v1/applications",
|
|
272
|
+
json={"name": "My App"},
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def test_create_interactive_with_values(
|
|
276
|
+
self, mock_api: MagicMock, tmp_path: Path
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Should send all interactively-provided values to the API."""
|
|
279
|
+
mock_api.post.return_value = {
|
|
280
|
+
"name": "My App",
|
|
281
|
+
"id": "abc123",
|
|
282
|
+
"client_secret": "secret456",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
with patch("sweatstack_cli.commands.app._is_interactive", return_value=True):
|
|
286
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
287
|
+
result = runner.invoke(
|
|
288
|
+
app,
|
|
289
|
+
["app", "create"],
|
|
290
|
+
input="My App\nA cool app\nmy-page\ny\n",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
assert result.exit_code == 0
|
|
294
|
+
mock_api.post.assert_called_once_with(
|
|
295
|
+
"/api/v1/applications",
|
|
296
|
+
json={
|
|
297
|
+
"name": "My App",
|
|
298
|
+
"description": "A cool app",
|
|
299
|
+
"page": "my-page",
|
|
300
|
+
"generate_secret": True,
|
|
301
|
+
},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def test_create_interactive_partial(
|
|
305
|
+
self, mock_api: MagicMock, tmp_path: Path
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Should only prompt for fields not provided via args/flags."""
|
|
308
|
+
mock_api.post.return_value = {"name": "My App", "id": "abc123"}
|
|
309
|
+
|
|
310
|
+
with patch("sweatstack_cli.commands.app._is_interactive", return_value=True):
|
|
311
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
312
|
+
result = runner.invoke(
|
|
313
|
+
app, ["app", "create", "My App"], input="\n\nn\n"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
assert result.exit_code == 0
|
|
317
|
+
assert "App name" not in result.stdout
|
|
318
|
+
mock_api.post.assert_called_once_with(
|
|
319
|
+
"/api/v1/applications",
|
|
320
|
+
json={"name": "My App"},
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
def test_create_interactive_skips_provided_flags(
|
|
324
|
+
self, mock_api: MagicMock, tmp_path: Path
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Should skip prompts for fields already provided via flags."""
|
|
327
|
+
mock_api.post.return_value = {
|
|
328
|
+
"name": "Test App",
|
|
329
|
+
"id": "abc123",
|
|
330
|
+
"client_secret": "secret456",
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
with patch("sweatstack_cli.commands.app._is_interactive", return_value=True):
|
|
334
|
+
with runner.isolated_filesystem(temp_dir=tmp_path):
|
|
335
|
+
# --secret and --description provided; only page is prompted
|
|
336
|
+
result = runner.invoke(
|
|
337
|
+
app,
|
|
338
|
+
["app", "create", "Test App", "--secret", "-d", "Desc"],
|
|
339
|
+
input="\n",
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
assert result.exit_code == 0
|
|
343
|
+
assert "Generate client secret?" not in result.stdout
|
|
344
|
+
assert "Description" not in result.stdout
|
|
345
|
+
mock_api.post.assert_called_once_with(
|
|
346
|
+
"/api/v1/applications",
|
|
347
|
+
json={
|
|
348
|
+
"name": "Test App",
|
|
349
|
+
"description": "Desc",
|
|
350
|
+
"generate_secret": True,
|
|
351
|
+
},
|
|
352
|
+
)
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|