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.
Files changed (51) hide show
  1. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/PKG-INFO +2 -2
  2. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/README.md +1 -1
  3. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/pyproject.toml +1 -1
  4. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/__init__.py +1 -1
  5. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/commands/app.py +33 -2
  6. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_commands/test_app.py +104 -1
  7. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/settings.local.json +0 -0
  8. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/SKILL.md +0 -0
  9. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/api.md +0 -0
  10. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/auth.md +0 -0
  11. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/cli.md +0 -0
  12. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/pyscript.md +0 -0
  13. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.claude/skills/sweatstack/webapp.md +0 -0
  14. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.gitignore +0 -0
  15. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/.python-version +0 -0
  16. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/CREATE_PRIVATE_APP.md +0 -0
  17. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/DEVELOPMENT.md +0 -0
  18. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/Makefile +0 -0
  19. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/PLAN.md +0 -0
  20. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/api/__init__.py +0 -0
  21. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/api/client.py +0 -0
  22. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/__init__.py +0 -0
  23. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/callback_server.py +0 -0
  24. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/jwt.py +0 -0
  25. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/pkce.py +0 -0
  26. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/auth/tokens.py +0 -0
  27. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/commands/__init__.py +0 -0
  28. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/commands/auth.py +0 -0
  29. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/commands/page.py +0 -0
  30. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/config.py +0 -0
  31. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/console.py +0 -0
  32. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/exceptions.py +0 -0
  33. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/main.py +0 -0
  34. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/project.py +0 -0
  35. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/py.typed +0 -0
  36. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/src/sweatstack_cli/version_check.py +0 -0
  37. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/sweatstack.toml +0 -0
  38. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/__init__.py +0 -0
  39. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/conftest.py +0 -0
  40. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_api/__init__.py +0 -0
  41. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_api/test_client.py +0 -0
  42. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/__init__.py +0 -0
  43. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/test_callback_server.py +0 -0
  44. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/test_jwt.py +0 -0
  45. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/test_pkce.py +0 -0
  46. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_auth/test_tokens.py +0 -0
  47. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_commands/__init__.py +0 -0
  48. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_commands/test_cli.py +0 -0
  49. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_project.py +0 -0
  50. {sweatstack_cli-0.8.0 → sweatstack_cli-0.9.0}/tests/test_version_check.py +0 -0
  51. {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.8.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack-cli"
3
- version = "0.8.0"
3
+ version = "0.9.0"
4
4
  description = "Command-line interface for SweatStack — the sports data platform for developers"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Aart Goossens", email = "aart@sweatstack.no" }]
@@ -1,3 +1,3 @@
1
1
  """SweatStack CLI — Command-line interface for the SweatStack platform."""
2
2
 
3
- __version__ = "0.8.0"
3
+ __version__ = "0.9.0"
@@ -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(..., help="Application name."),
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 sys.stdin.isatty():
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