sweatstack-cli 0.4.1__py3-none-any.whl → 0.6.0__py3-none-any.whl
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/__init__.py +1 -1
- sweatstack_cli/commands/app.py +82 -5
- sweatstack_cli/commands/auth.py +1 -1
- sweatstack_cli/commands/page.py +31 -11
- sweatstack_cli/project.py +111 -0
- sweatstack_cli/version_check.py +2 -2
- {sweatstack_cli-0.4.1.dist-info → sweatstack_cli-0.6.0.dist-info}/METADATA +3 -3
- {sweatstack_cli-0.4.1.dist-info → sweatstack_cli-0.6.0.dist-info}/RECORD +10 -9
- {sweatstack_cli-0.4.1.dist-info → sweatstack_cli-0.6.0.dist-info}/WHEEL +1 -1
- {sweatstack_cli-0.4.1.dist-info → sweatstack_cli-0.6.0.dist-info}/entry_points.txt +0 -0
sweatstack_cli/__init__.py
CHANGED
sweatstack_cli/commands/app.py
CHANGED
|
@@ -4,12 +4,14 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json as json_lib
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import Any, cast
|
|
7
8
|
|
|
8
9
|
import typer
|
|
9
10
|
|
|
10
11
|
from sweatstack_cli.api import APIClient
|
|
11
12
|
from sweatstack_cli.console import console, stderr_console
|
|
12
13
|
from sweatstack_cli.exceptions import APIError, AuthenticationError
|
|
14
|
+
from sweatstack_cli.project import CONFIG_FILENAME, write_config
|
|
13
15
|
|
|
14
16
|
app = typer.Typer(name="app", help="Manage OAuth2 applications.")
|
|
15
17
|
|
|
@@ -75,13 +77,20 @@ def create(
|
|
|
75
77
|
if env_path:
|
|
76
78
|
_write_env_file(env_path, result)
|
|
77
79
|
|
|
80
|
+
# Write sweatstack.toml
|
|
81
|
+
config_path = write_config(
|
|
82
|
+
app_name=result["name"],
|
|
83
|
+
client_id=result["client_id"],
|
|
84
|
+
page_slug=page,
|
|
85
|
+
)
|
|
86
|
+
|
|
78
87
|
if json_output:
|
|
79
88
|
console.print(json_lib.dumps(result, indent=2))
|
|
80
89
|
else:
|
|
81
|
-
_print_app_created(result, env_path)
|
|
90
|
+
_print_app_created(result, env_path, config_path)
|
|
82
91
|
|
|
83
92
|
|
|
84
|
-
def _write_env_file(path: Path, result: dict) -> None:
|
|
93
|
+
def _write_env_file(path: Path, result: dict[str, str]) -> None:
|
|
85
94
|
"""Append credentials to an env file."""
|
|
86
95
|
lines_to_add: list[str] = []
|
|
87
96
|
existing_content = ""
|
|
@@ -116,7 +125,7 @@ def _write_env_file(path: Path, result: dict) -> None:
|
|
|
116
125
|
f.write("\n".join(lines_to_add) + "\n")
|
|
117
126
|
|
|
118
127
|
|
|
119
|
-
def _print_app_created(result: dict, env_path: Path | None) -> None:
|
|
128
|
+
def _print_app_created(result: dict[str, str], env_path: Path | None, config_path: Path) -> None:
|
|
120
129
|
"""Pretty-print the created application details."""
|
|
121
130
|
console.print(f"[green]✓[/green] Created application [bold]{result['name']}[/bold]")
|
|
122
131
|
console.print()
|
|
@@ -131,9 +140,77 @@ def _print_app_created(result: dict, env_path: Path | None) -> None:
|
|
|
131
140
|
for uri in result["redirect_uris"]:
|
|
132
141
|
console.print(f" - {uri}")
|
|
133
142
|
|
|
143
|
+
console.print()
|
|
144
|
+
console.print(f"[green]✓[/green] Config written to [bold]{config_path}[/bold]")
|
|
145
|
+
|
|
134
146
|
if env_path:
|
|
135
|
-
console.print()
|
|
136
147
|
console.print(f"[green]✓[/green] Credentials written to [bold]{env_path}[/bold]")
|
|
137
|
-
|
|
148
|
+
|
|
149
|
+
if "client_secret" in result and not env_path:
|
|
138
150
|
console.print()
|
|
139
151
|
console.print("[yellow]Save the secret now — it won't be shown again.[/yellow]")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command()
|
|
155
|
+
def link(
|
|
156
|
+
force: bool = typer.Option(
|
|
157
|
+
False,
|
|
158
|
+
"--force",
|
|
159
|
+
"-f",
|
|
160
|
+
help="Overwrite existing sweatstack.toml.",
|
|
161
|
+
),
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Link this directory to an existing application."""
|
|
164
|
+
config_path = Path(CONFIG_FILENAME)
|
|
165
|
+
|
|
166
|
+
# Check if config already exists
|
|
167
|
+
if config_path.exists() and not force:
|
|
168
|
+
console.print(f"[yellow]Already linked.[/yellow] {CONFIG_FILENAME} exists.")
|
|
169
|
+
console.print("Use [cyan]--force[/cyan] to overwrite.")
|
|
170
|
+
raise typer.Exit(1)
|
|
171
|
+
|
|
172
|
+
# Fetch user's apps
|
|
173
|
+
try:
|
|
174
|
+
client = APIClient()
|
|
175
|
+
except AuthenticationError as e:
|
|
176
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
177
|
+
console.print("[dim]Run 'sweatstack login' to authenticate.[/dim]")
|
|
178
|
+
raise typer.Exit(2)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
# API returns a list, but client.get is typed as returning dict
|
|
182
|
+
apps = cast(list[dict[str, Any]], client.get("/api/v1/applications"))
|
|
183
|
+
except APIError as e:
|
|
184
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
185
|
+
raise typer.Exit(3)
|
|
186
|
+
|
|
187
|
+
if not apps:
|
|
188
|
+
console.print("[yellow]No applications found.[/yellow]")
|
|
189
|
+
console.print("Create one with [cyan]sweatstack app create <name>[/cyan]")
|
|
190
|
+
raise typer.Exit(1)
|
|
191
|
+
|
|
192
|
+
# Show apps and let user select
|
|
193
|
+
console.print("Link this directory to an existing application.\n")
|
|
194
|
+
console.print("Your applications:")
|
|
195
|
+
for i, app_info in enumerate(apps, 1):
|
|
196
|
+
page_info = f" [dim](page: {app_info['page_slug']})[/dim]" if app_info.get("page_slug") else ""
|
|
197
|
+
console.print(f" [cyan][{i}][/cyan] {app_info['name']}{page_info}")
|
|
198
|
+
|
|
199
|
+
console.print()
|
|
200
|
+
selection = typer.prompt("Select application", type=int)
|
|
201
|
+
|
|
202
|
+
if selection < 1 or selection > len(apps):
|
|
203
|
+
console.print("[red]Invalid selection.[/red]")
|
|
204
|
+
raise typer.Exit(1)
|
|
205
|
+
|
|
206
|
+
selected = apps[selection - 1]
|
|
207
|
+
|
|
208
|
+
# Write config
|
|
209
|
+
write_config(
|
|
210
|
+
app_name=selected["name"],
|
|
211
|
+
client_id=selected["id"],
|
|
212
|
+
page_slug=selected.get("page_slug"),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
console.print()
|
|
216
|
+
console.print(f"[green]✓[/green] Linked to [bold]{selected['name']}[/bold]")
|
sweatstack_cli/commands/auth.py
CHANGED
|
@@ -87,7 +87,7 @@ def status() -> None:
|
|
|
87
87
|
if update_available and latest:
|
|
88
88
|
console.print(
|
|
89
89
|
f"[yellow]⚠[/yellow] Update available: {latest} (current: {__version__})\n"
|
|
90
|
-
f" Run: [cyan]
|
|
90
|
+
f" Run: [cyan]uv tool upgrade sweatstack-cli[/cyan] [dim](or pip/pipx)[/dim]"
|
|
91
91
|
)
|
|
92
92
|
elif latest:
|
|
93
93
|
console.print(f"[green]✓[/green] Up to date ({__version__})")
|
sweatstack_cli/commands/page.py
CHANGED
|
@@ -11,6 +11,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
|
11
11
|
from sweatstack_cli.api import APIClient
|
|
12
12
|
from sweatstack_cli.console import console
|
|
13
13
|
from sweatstack_cli.exceptions import APIError, AuthenticationError
|
|
14
|
+
from sweatstack_cli.project import CONFIG_FILENAME, ProjectConfig
|
|
14
15
|
|
|
15
16
|
# URL where users can create pages for their applications
|
|
16
17
|
SETTINGS_URL = "https://app.sweatstack.no/settings/api"
|
|
@@ -53,21 +54,40 @@ def _collect_files(directory: Path) -> list[tuple[str, tuple[str, bytes, str]]]:
|
|
|
53
54
|
|
|
54
55
|
@app.command()
|
|
55
56
|
def deploy(
|
|
56
|
-
slug: str = typer.Argument(
|
|
57
|
-
|
|
58
|
-
help="Page slug
|
|
57
|
+
slug: str | None = typer.Argument(
|
|
58
|
+
None,
|
|
59
|
+
help="Page slug. Reads from sweatstack.toml if not provided.",
|
|
59
60
|
),
|
|
60
|
-
directory: Path = typer.Argument(
|
|
61
|
-
|
|
62
|
-
help="Directory
|
|
63
|
-
exists=True,
|
|
64
|
-
file_okay=False,
|
|
65
|
-
dir_okay=True,
|
|
66
|
-
resolve_path=True,
|
|
61
|
+
directory: Path | None = typer.Argument(
|
|
62
|
+
None,
|
|
63
|
+
help="Directory to deploy. Reads from sweatstack.toml or defaults to current dir.",
|
|
67
64
|
),
|
|
68
65
|
) -> None:
|
|
69
66
|
"""Deploy a static site to SweatStack Pages."""
|
|
70
|
-
|
|
67
|
+
# Load config for defaults
|
|
68
|
+
config = ProjectConfig.load()
|
|
69
|
+
|
|
70
|
+
# Resolve slug
|
|
71
|
+
page_slug = slug or (config.page.slug if config else None)
|
|
72
|
+
if not page_slug:
|
|
73
|
+
console.print("[red]Error:[/red] No page slug provided.")
|
|
74
|
+
console.print(f"Specify a slug or add it to [bold]{CONFIG_FILENAME}[/bold]")
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
|
|
77
|
+
# Resolve directory
|
|
78
|
+
if directory is None:
|
|
79
|
+
dir_from_config = config.page.directory if config else None
|
|
80
|
+
directory = Path(dir_from_config) if dir_from_config else Path(".")
|
|
81
|
+
|
|
82
|
+
if not directory.exists():
|
|
83
|
+
console.print(f"[red]Error:[/red] Directory '{directory}' does not exist.")
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
if not directory.is_dir():
|
|
87
|
+
console.print(f"[red]Error:[/red] '{directory}' is not a directory.")
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
|
|
90
|
+
directory = directory.resolve()
|
|
71
91
|
|
|
72
92
|
# Collect files
|
|
73
93
|
files = _collect_files(directory)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Project configuration via sweatstack.toml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
CONFIG_FILENAME = "sweatstack.toml"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class PageConfig:
|
|
14
|
+
"""Page configuration."""
|
|
15
|
+
|
|
16
|
+
slug: str | None = None
|
|
17
|
+
directory: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AppConfig:
|
|
22
|
+
"""Application configuration."""
|
|
23
|
+
|
|
24
|
+
name: str | None = None
|
|
25
|
+
client_id: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ProjectConfig:
|
|
30
|
+
"""Project configuration from sweatstack.toml."""
|
|
31
|
+
|
|
32
|
+
app: AppConfig
|
|
33
|
+
page: PageConfig
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def load(cls, path: Path | None = None) -> ProjectConfig | None:
|
|
37
|
+
"""
|
|
38
|
+
Load config from sweatstack.toml in current directory.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
path: Optional path to config file. Defaults to ./sweatstack.toml.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
ProjectConfig if file exists and is valid, None otherwise.
|
|
45
|
+
"""
|
|
46
|
+
config_path = path or Path(CONFIG_FILENAME)
|
|
47
|
+
|
|
48
|
+
if not config_path.exists():
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
data = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
|
53
|
+
except tomllib.TOMLDecodeError:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
app_data = data.get("app", {})
|
|
57
|
+
page_data = data.get("page", {})
|
|
58
|
+
|
|
59
|
+
return cls(
|
|
60
|
+
app=AppConfig(
|
|
61
|
+
name=app_data.get("name"),
|
|
62
|
+
client_id=app_data.get("client_id"),
|
|
63
|
+
),
|
|
64
|
+
page=PageConfig(
|
|
65
|
+
slug=page_data.get("slug"),
|
|
66
|
+
directory=page_data.get("directory"),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def write_config(
|
|
72
|
+
path: Path | None = None,
|
|
73
|
+
*,
|
|
74
|
+
app_name: str | None = None,
|
|
75
|
+
client_id: str | None = None,
|
|
76
|
+
page_slug: str | None = None,
|
|
77
|
+
page_directory: str | None = None,
|
|
78
|
+
) -> Path:
|
|
79
|
+
"""
|
|
80
|
+
Write sweatstack.toml file.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
path: Optional path to config file. Defaults to ./sweatstack.toml.
|
|
84
|
+
app_name: Application name.
|
|
85
|
+
client_id: OAuth2 client ID.
|
|
86
|
+
page_slug: Page slug.
|
|
87
|
+
page_directory: Page directory.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Path to the written config file.
|
|
91
|
+
"""
|
|
92
|
+
config_path = path or Path(CONFIG_FILENAME)
|
|
93
|
+
|
|
94
|
+
lines = ["[app]"]
|
|
95
|
+
if app_name:
|
|
96
|
+
lines.append(f'name = "{app_name}"')
|
|
97
|
+
if client_id:
|
|
98
|
+
lines.append(f'client_id = "{client_id}"')
|
|
99
|
+
|
|
100
|
+
if page_slug or page_directory:
|
|
101
|
+
lines.append("")
|
|
102
|
+
lines.append("[page]")
|
|
103
|
+
if page_slug:
|
|
104
|
+
lines.append(f'slug = "{page_slug}"')
|
|
105
|
+
if page_directory:
|
|
106
|
+
lines.append(f'directory = "{page_directory}"')
|
|
107
|
+
|
|
108
|
+
lines.append("") # Trailing newline
|
|
109
|
+
config_path.write_text("\n".join(lines), encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
return config_path
|
sweatstack_cli/version_check.py
CHANGED
|
@@ -70,6 +70,6 @@ def get_update_message() -> str | None:
|
|
|
70
70
|
return None
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
|
-
f"
|
|
74
|
-
f"
|
|
73
|
+
f"Update available: {latest} (current: {__version__})\n"
|
|
74
|
+
f"Run: [cyan]uv tool upgrade sweatstack-cli[/cyan] [dim](or pip/pipx)[/dim]"
|
|
75
75
|
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sweatstack-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
7
7
|
Project-URL: Repository, https://github.com/sweatstack/sweatstack-cli
|
|
8
|
-
Author-email: Aart Goossens <aart@
|
|
8
|
+
Author-email: Aart Goossens <aart@sweatstack.no>
|
|
9
9
|
License: MIT
|
|
10
10
|
Keywords: api,cli,fitness,sports,sweatstack
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -16,7 +16,7 @@ Classifier: Operating System :: OS Independent
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
|
-
Requires-Python: >=3.
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
20
|
Requires-Dist: httpx>=0.28.0
|
|
21
21
|
Requires-Dist: platformdirs>=4.0
|
|
22
22
|
Requires-Dist: typer>=0.15.0
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
sweatstack_cli/__init__.py,sha256=
|
|
1
|
+
sweatstack_cli/__init__.py,sha256=_S69yAFZ4csuAK-vZLnKptWD6bgZhmSmo0USYOddmhM,100
|
|
2
2
|
sweatstack_cli/config.py,sha256=cD4OXnVAKVagYazyk9Cw-WWx62tJPqcjQt1cknYyymM,706
|
|
3
3
|
sweatstack_cli/console.py,sha256=69KWAM7ksnYRTONMYTXGXN4LWRAQhRxcLXO0LcWGLqo,300
|
|
4
4
|
sweatstack_cli/exceptions.py,sha256=qEUu8GydXGRKR4ndAdZ2wnkHekmFpBXDJiT5bXdl7Yk,613
|
|
5
5
|
sweatstack_cli/main.py,sha256=obcF1VGhwcE_pI1BwTJkY4J4BgIGac-y3yYGvY6ZOp8,2330
|
|
6
|
+
sweatstack_cli/project.py,sha256=mOwqzuc6D6J6IHf-5L9Vv_sfjpReS4vwzSjUSFi7qys,2716
|
|
6
7
|
sweatstack_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
sweatstack_cli/version_check.py,sha256=
|
|
8
|
+
sweatstack_cli/version_check.py,sha256=yZ_qVe7tUBowUDAmMki-wyf5pSxlNDsoWKgTcSfesBA,2015
|
|
8
9
|
sweatstack_cli/api/__init__.py,sha256=l9s6BzF6Wx17K_ElRUX-8aAwwIIzqmGF1PQ3Mbc07mw,107
|
|
9
10
|
sweatstack_cli/api/client.py,sha256=X_c0eYn-mN76nMWidmFgmJ4rvkQlKCK-kMrPmXBybOM,6075
|
|
10
11
|
sweatstack_cli/auth/__init__.py,sha256=J5WTFjaogP5lPTorWcW3VTwok-9kI_NxFLtXBYWQCI4,6926
|
|
@@ -13,10 +14,10 @@ sweatstack_cli/auth/jwt.py,sha256=Ja6ccFyRKtX5MPjFdHNavVYf8KTz0MYT86StbnF-cwg,15
|
|
|
13
14
|
sweatstack_cli/auth/pkce.py,sha256=sO_xUeN1UIvXKX0yEIGgMTIIzvfCOCUwlyNAveEd16k,1542
|
|
14
15
|
sweatstack_cli/auth/tokens.py,sha256=kc8ytoO9PAyjLD6wUTIagVuP7_ia5ZjW5m_WjXIRIkA,4942
|
|
15
16
|
sweatstack_cli/commands/__init__.py,sha256=YCO8wYVvdGeoEI0p_oUm8ANLneuET4SHsKDjitMjAgY,35
|
|
16
|
-
sweatstack_cli/commands/app.py,sha256=
|
|
17
|
-
sweatstack_cli/commands/auth.py,sha256=
|
|
18
|
-
sweatstack_cli/commands/page.py,sha256=
|
|
19
|
-
sweatstack_cli-0.
|
|
20
|
-
sweatstack_cli-0.
|
|
21
|
-
sweatstack_cli-0.
|
|
22
|
-
sweatstack_cli-0.
|
|
17
|
+
sweatstack_cli/commands/app.py,sha256=Km04lK_1Ur6eaIruLSqQ0mQ5uK9W5ru6PrtDB-XZCD4,6721
|
|
18
|
+
sweatstack_cli/commands/auth.py,sha256=lmlN2RcZIHdbZG-EEUVFwOSn6vPN-Ijgh_5E-cFQVtU,3478
|
|
19
|
+
sweatstack_cli/commands/page.py,sha256=FNcbNmslBcPd3GoObgPsznm7LKxi3pnldNdOaRCmcwA,4836
|
|
20
|
+
sweatstack_cli-0.6.0.dist-info/METADATA,sha256=KNbChDCKMU5RyM2XaMx6dr-QtxHZnZZSqTjHHUOeHAM,3576
|
|
21
|
+
sweatstack_cli-0.6.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
22
|
+
sweatstack_cli-0.6.0.dist-info/entry_points.txt,sha256=rnH0EV1U9CQlmKQcJ7RmxGpNRTa9J-YdiIJZDYrAXb4,56
|
|
23
|
+
sweatstack_cli-0.6.0.dist-info/RECORD,,
|
|
File without changes
|