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.
@@ -1,3 +1,3 @@
1
1
  """SweatStack CLI — Command-line interface for the SweatStack platform."""
2
2
 
3
- __version__ = "0.4.1"
3
+ __version__ = "0.6.0"
@@ -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
- elif "client_secret" in result:
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]")
@@ -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]pip install --upgrade sweatstack-cli[/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__})")
@@ -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 (must be created first at app.sweatstack.no).",
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 containing static files to deploy.",
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
- page_slug = slug
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
@@ -70,6 +70,6 @@ def get_update_message() -> str | None:
70
70
  return None
71
71
 
72
72
  return (
73
- f"A new version of sweatstack-cli is available: {latest} (current: {__version__})\n"
74
- f"Update with: [cyan]pip install --upgrade sweatstack-cli[/cyan]"
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.4.1
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@goossens.me>
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.10
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=IgwjheeK6yl8LyQnqcOi_92-yy2ENIOqBcvSaBdTnic,100
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=ozRFZoSqtc2ROekntLiqEQ-LQb2VObjlCgwclVKtw-c,2032
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=fOdnaX1yYogo_Gire6VwBXizOXOiFLZHBd9v65s-wgA,4189
17
- sweatstack_cli/commands/auth.py,sha256=5EW800obVlX6474UiXByNXlCI5jEKdvUFP-y9Wtw3oY,3459
18
- sweatstack_cli/commands/page.py,sha256=PX3KPMgf6uSBtWioHOl0YeRqaWEwSOGbr7xRJwsdtU0,3978
19
- sweatstack_cli-0.4.1.dist-info/METADATA,sha256=cJ_Qx3kslp6FKxIviDerFTRYfy2Aytb4WX1__3r1J9Y,3574
20
- sweatstack_cli-0.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
21
- sweatstack_cli-0.4.1.dist-info/entry_points.txt,sha256=rnH0EV1U9CQlmKQcJ7RmxGpNRTa9J-YdiIJZDYrAXb4,56
22
- sweatstack_cli-0.4.1.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any