sweatstack-cli 0.3.0__tar.gz → 0.4.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 (43) hide show
  1. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/PKG-INFO +1 -1
  2. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/pyproject.toml +1 -1
  3. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/__init__.py +1 -1
  4. sweatstack_cli-0.4.0/src/sweatstack_cli/commands/auth.py +100 -0
  5. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/main.py +1 -2
  6. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_commands/test_cli.py +1 -1
  7. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/uv.lock +1 -1
  8. sweatstack_cli-0.3.0/src/sweatstack_cli/commands/auth.py +0 -127
  9. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/.claude/settings.local.json +0 -0
  10. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/.gitignore +0 -0
  11. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/.python-version +0 -0
  12. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/CREATE_PRIVATE_APP.md +0 -0
  13. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/DEVELOPMENT.md +0 -0
  14. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/Makefile +0 -0
  15. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/PLAN.md +0 -0
  16. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/README.md +0 -0
  17. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/api/__init__.py +0 -0
  18. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/api/client.py +0 -0
  19. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/auth/__init__.py +0 -0
  20. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/auth/callback_server.py +0 -0
  21. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/auth/jwt.py +0 -0
  22. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/auth/pkce.py +0 -0
  23. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/auth/tokens.py +0 -0
  24. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/commands/__init__.py +0 -0
  25. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/commands/app.py +0 -0
  26. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/commands/page.py +0 -0
  27. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/config.py +0 -0
  28. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/console.py +0 -0
  29. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/exceptions.py +0 -0
  30. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/py.typed +0 -0
  31. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/src/sweatstack_cli/version_check.py +0 -0
  32. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/__init__.py +0 -0
  33. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/conftest.py +0 -0
  34. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_api/__init__.py +0 -0
  35. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_api/test_client.py +0 -0
  36. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_auth/__init__.py +0 -0
  37. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_auth/test_callback_server.py +0 -0
  38. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_auth/test_jwt.py +0 -0
  39. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_auth/test_pkce.py +0 -0
  40. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_auth/test_tokens.py +0 -0
  41. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_commands/__init__.py +0 -0
  42. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_commands/test_app.py +0 -0
  43. {sweatstack_cli-0.3.0 → sweatstack_cli-0.4.0}/tests/test_version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sweatstack-cli
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sweatstack-cli"
3
- version = "0.3.0"
3
+ version = "0.4.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@goossens.me" }]
@@ -1,3 +1,3 @@
1
1
  """SweatStack CLI — Command-line interface for the SweatStack platform."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.0"
@@ -0,0 +1,100 @@
1
+ """Authentication commands: login, logout, whoami, status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from sweatstack_cli.api import APIClient
8
+ from sweatstack_cli.auth import Authenticator
9
+ from sweatstack_cli.console import console
10
+ from sweatstack_cli.exceptions import APIError, AuthenticationError
11
+ from sweatstack_cli.version_check import check_for_update
12
+
13
+
14
+ def login(
15
+ force: bool = typer.Option(
16
+ False,
17
+ "--force",
18
+ "-f",
19
+ help="Force re-authentication even if already logged in.",
20
+ ),
21
+ ) -> None:
22
+ """Authenticate with SweatStack via browser."""
23
+ auth = Authenticator()
24
+
25
+ # Check if already logged in
26
+ if not force:
27
+ existing = auth.get_valid_tokens()
28
+ if existing:
29
+ try:
30
+ client = APIClient(authenticator=auth)
31
+ user = client.get("/api/v1/oauth/userinfo")
32
+ console.print(
33
+ f"Already logged in as [bold]{user['name']}[/bold]. "
34
+ f"Use [cyan]--force[/cyan] to re-authenticate."
35
+ )
36
+ return
37
+ except AuthenticationError:
38
+ pass # Token invalid, proceed with login
39
+
40
+ with console.status("[bold]Opening browser for authentication...[/bold]"):
41
+ try:
42
+ auth.login(force=True)
43
+ except AuthenticationError as e:
44
+ console.print(f"[red]Authentication failed:[/red] {e}")
45
+ raise typer.Exit(2)
46
+
47
+ # Verify by fetching user info
48
+ try:
49
+ client = APIClient(authenticator=auth)
50
+ user = client.get("/api/v1/oauth/userinfo")
51
+ console.print(f"[green]✓[/green] Logged in as [bold]{user['name']}[/bold]")
52
+ except AuthenticationError as e:
53
+ console.print(f"[red]Login succeeded but verification failed:[/red] {e}")
54
+ raise typer.Exit(2)
55
+
56
+
57
+ def logout() -> None:
58
+ """Remove stored credentials."""
59
+ auth = Authenticator()
60
+ auth.logout()
61
+ console.print("[green]✓[/green] Logged out")
62
+
63
+
64
+ def status() -> None:
65
+ """Show authentication status and version info."""
66
+ from sweatstack_cli import __version__
67
+
68
+ # Check authentication and get user info
69
+ auth = Authenticator()
70
+ tokens = auth.get_valid_tokens()
71
+
72
+ if tokens is None:
73
+ console.print("[red]✗[/red] Not authenticated")
74
+ else:
75
+ console.print("[green]✓[/green] Authenticated")
76
+ try:
77
+ client = APIClient(authenticator=auth)
78
+ user = client.get("/api/v1/oauth/userinfo")
79
+ console.print(f" Name: {user.get('name', 'unknown')}")
80
+ console.print(f" Email: {user.get('email', 'unknown')}")
81
+ console.print(f" ID: {user.get('sub', 'unknown')}")
82
+ except (AuthenticationError, APIError):
83
+ pass # Silently skip user info on error
84
+
85
+ # Check version
86
+ update_available, latest = check_for_update()
87
+ if update_available and latest:
88
+ console.print(
89
+ f"[yellow]⚠[/yellow] Update available: {latest} (current: {__version__})\n"
90
+ f" Run: [cyan]pip install --upgrade sweatstack-cli[/cyan]"
91
+ )
92
+ elif latest:
93
+ console.print(f"[green]✓[/green] Up to date ({__version__})")
94
+ else:
95
+ console.print(f"[dim]?[/dim] Version {__version__} (update check failed)")
96
+
97
+ # Exit with error if not authenticated
98
+ if tokens is None:
99
+ console.print("\nRun [bold]sweatstack login[/bold] to authenticate.")
100
+ raise typer.Exit(1)
@@ -28,8 +28,7 @@ app.add_typer(app_commands.app, name="app")
28
28
  # These are the most common operations, so we promote them to top level
29
29
  app.command(name="login", help="Authenticate with SweatStack via browser.")(auth.login)
30
30
  app.command(name="logout", help="Remove stored credentials.")(auth.logout)
31
- app.command(name="whoami", help="Show current authenticated user.")(auth.whoami)
32
- app.command(name="status", help="Show authentication status and token details.")(auth.status)
31
+ app.command(name="status", help="Show authentication and version status.")(auth.status)
33
32
 
34
33
 
35
34
  def version_callback(value: bool) -> None:
@@ -20,7 +20,7 @@ class TestCLI:
20
20
  assert "SweatStack CLI" in result.stdout
21
21
  assert "login" in result.stdout
22
22
  assert "logout" in result.stdout
23
- assert "whoami" in result.stdout
23
+ assert "status" in result.stdout
24
24
  assert "page" in result.stdout
25
25
 
26
26
  def test_version(self) -> None:
@@ -509,7 +509,7 @@ wheels = [
509
509
 
510
510
  [[package]]
511
511
  name = "sweatstack-cli"
512
- version = "0.3.0"
512
+ version = "0.4.0"
513
513
  source = { editable = "." }
514
514
  dependencies = [
515
515
  { name = "httpx" },
@@ -1,127 +0,0 @@
1
- """Authentication commands: login, logout, whoami, status."""
2
-
3
- from __future__ import annotations
4
-
5
- import datetime
6
-
7
- import typer
8
-
9
- from sweatstack_cli.api import APIClient
10
- from sweatstack_cli.auth import Authenticator
11
- from sweatstack_cli.auth.jwt import decode_jwt_payload
12
- from sweatstack_cli.console import console
13
- from sweatstack_cli.exceptions import APIError, AuthenticationError
14
-
15
-
16
- def login(
17
- force: bool = typer.Option(
18
- False,
19
- "--force",
20
- "-f",
21
- help="Force re-authentication even if already logged in.",
22
- ),
23
- ) -> None:
24
- """Authenticate with SweatStack via browser."""
25
- auth = Authenticator()
26
-
27
- # Check if already logged in
28
- if not force:
29
- existing = auth.get_valid_tokens()
30
- if existing:
31
- try:
32
- client = APIClient(authenticator=auth)
33
- user = client.get("/api/v1/oauth/userinfo")
34
- console.print(
35
- f"Already logged in as [bold]{user['name']}[/bold]. "
36
- f"Use [cyan]--force[/cyan] to re-authenticate."
37
- )
38
- return
39
- except AuthenticationError:
40
- pass # Token invalid, proceed with login
41
-
42
- with console.status("[bold]Opening browser for authentication...[/bold]"):
43
- try:
44
- auth.login(force=True)
45
- except AuthenticationError as e:
46
- console.print(f"[red]Authentication failed:[/red] {e}")
47
- raise typer.Exit(2)
48
-
49
- # Verify by fetching user info
50
- try:
51
- client = APIClient(authenticator=auth)
52
- user = client.get("/api/v1/oauth/userinfo")
53
- console.print(f"[green]✓[/green] Logged in as [bold]{user['name']}[/bold]")
54
- except AuthenticationError as e:
55
- console.print(f"[red]Login succeeded but verification failed:[/red] {e}")
56
- raise typer.Exit(2)
57
-
58
-
59
- def logout() -> None:
60
- """Remove stored credentials."""
61
- auth = Authenticator()
62
- auth.logout()
63
- console.print("[green]✓[/green] Logged out")
64
-
65
-
66
- def whoami() -> None:
67
- """Show current authenticated user."""
68
- try:
69
- client = APIClient()
70
- user = client.get("/api/v1/oauth/userinfo")
71
- console.print(
72
- f"Logged in as: [bold]{user['name']}[/bold] (email={user.get('email', 'no email')}, id={user.get('sub', 'no id')})"
73
- )
74
- except AuthenticationError:
75
- console.print("[yellow]Not logged in[/yellow]")
76
- console.print("Run [bold]sweatstack login[/bold] to authenticate.")
77
- raise typer.Exit(1)
78
- except APIError as e:
79
- console.print(f"[red]Error:[/red] {e}")
80
- raise typer.Exit(3)
81
-
82
-
83
- def status() -> None:
84
- """Show authentication status and token details."""
85
- auth = Authenticator()
86
- tokens = auth.get_valid_tokens()
87
-
88
- if tokens is None:
89
- console.print("[yellow]Not authenticated[/yellow]")
90
- console.print("Run [bold]sweatstack login[/bold] to authenticate.")
91
- raise typer.Exit(1)
92
-
93
- try:
94
- payload = decode_jwt_payload(tokens.access_token)
95
- except ValueError as e:
96
- console.print(f"[red]Invalid token format:[/red] {e}")
97
- console.print("Run [bold]sweatstack login --force[/bold] to re-authenticate.")
98
- raise typer.Exit(2)
99
-
100
- # Format expiry time
101
- exp_timestamp = payload.get("exp", 0)
102
- exp_dt = datetime.datetime.fromtimestamp(exp_timestamp, tz=datetime.UTC)
103
- now = datetime.datetime.now(tz=datetime.UTC)
104
-
105
- if exp_dt > now:
106
- remaining = exp_dt - now
107
- hours, remainder = divmod(int(remaining.total_seconds()), 3600)
108
- minutes, seconds = divmod(remainder, 60)
109
-
110
- if hours > 0:
111
- time_str = f"{hours}h {minutes}m remaining"
112
- elif minutes > 0:
113
- time_str = f"{minutes}m {seconds}s remaining"
114
- else:
115
- time_str = f"{seconds}s remaining"
116
-
117
- expiry_display = f"{exp_dt.strftime('%Y-%m-%d %H:%M:%S')} UTC ({time_str})"
118
- else:
119
- expiry_display = f"{exp_dt.strftime('%Y-%m-%d %H:%M:%S')} UTC [red](expired)[/red]"
120
-
121
- console.print("[green]✓[/green] Authenticated")
122
- console.print(f" User ID: {payload.get('sub', 'unknown')}")
123
- console.print(f" Expires: {expiry_display}")
124
-
125
- # Show timezone if available
126
- if tz := payload.get("tz"):
127
- console.print(f" Timezone: {tz}")
File without changes
File without changes
File without changes