flowstash-cli 0.2.13__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 (53) hide show
  1. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/PKG-INFO +2 -2
  2. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/pyproject.toml +2 -2
  3. flowstash_cli-0.4.0/src/flowstash/cli/commands/apikey.py +273 -0
  4. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/build.py +44 -20
  5. flowstash_cli-0.4.0/src/flowstash/cli/commands/client.py +479 -0
  6. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/project.py +26 -217
  7. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/main.py +22 -1
  8. flowstash_cli-0.4.0/src/flowstash/cli/templates/_config/shared/backend.yaml +80 -0
  9. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/shared/clients/demoClient.yaml +16 -1
  10. flowstash_cli-0.2.13/src/flowstash/cli/templates/_config/[env]/.env +0 -3
  11. flowstash_cli-0.2.13/src/flowstash/cli/templates/_config/shared/.env +0 -2
  12. flowstash_cli-0.2.13/src/flowstash/cli/templates/_config/shared/backend.yaml +0 -7
  13. flowstash_cli-0.2.13/src/flowstash/cli/templates/_deployment/shared/.env +0 -5
  14. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/__init__.py +0 -0
  15. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/__init__.py +0 -0
  16. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/auth.py +0 -0
  17. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/deploy.py +0 -0
  18. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/run.py +0 -0
  19. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/webhook.py +0 -0
  20. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/__init__.py +0 -0
  21. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/api_client.py +0 -0
  22. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/auth_server.py +0 -0
  23. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/builder.py +0 -0
  24. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/config.py +0 -0
  25. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/docker_utils.py +0 -0
  26. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/patcher.py +0 -0
  27. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/AGENTS.md +0 -0
  28. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/README.md +0 -0
  29. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_.dockerignore +0 -0
  30. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_.flowstash +0 -0
  31. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_api_main.py +0 -0
  32. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(backend-asyncio)/backend.yaml +0 -0
  33. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(backend-dramatiq)/backend.yaml +0 -0
  34. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(backend-managed)/backend.yaml +0 -0
  35. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(observability-logfile)/observability.yaml +0 -0
  36. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(observability-managed)/observability.yaml +0 -0
  37. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/_backend.yaml +0 -0
  38. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/shared/clients.yaml +0 -0
  39. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +0 -0
  40. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +0 -0
  41. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_deployment/shared/api.Dockerfile +0 -0
  42. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_deployment/shared/worker.Dockerfile +0 -0
  43. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_pyproject.toml +0 -0
  44. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_api/__init__.py +0 -0
  45. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_api/_routes/webhooks.py +0 -0
  46. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_shared/__init__.py +0 -0
  47. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_shared/clients/client.py +0 -0
  48. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_shared/models/models.py +0 -0
  49. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_shared/tasks/sharedTasks.py +0 -0
  50. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_worker/__init__.py +0 -0
  51. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_worker/tasks/tasks.py +0 -0
  52. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_worker_main.py +0 -0
  53. {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/ui/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flowstash-cli
3
- Version: 0.2.13
3
+ Version: 0.4.0
4
4
  Summary: CLI for the flowstash Managed Platform
5
5
  Author: juraj.bezdek@gmail.com
6
6
  Author-email: juraj.bezdek@gmail.com
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
- Requires-Dist: flowstash-runtime (>=0.2.13,<0.3.0)
12
+ Requires-Dist: flowstash-runtime (>=0.4.0,<0.5.0)
13
13
  Requires-Dist: httpx (>=0.27.0)
14
14
  Requires-Dist: keyring (>=25.0.0)
15
15
  Requires-Dist: libcst (>=1.1.0)
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "flowstash-cli"
3
- version = "0.2.13"
3
+ version = "0.4.0"
4
4
  description = "CLI for the flowstash Managed Platform"
5
5
  authors = [{name = "juraj.bezdek@gmail.com", email = "juraj.bezdek@gmail.com"}]
6
6
  requires-python = ">=3.11"
7
7
  dependencies = [
8
- "flowstash-runtime>=0.2.13,<0.3.0",
8
+ "flowstash-runtime>=0.4.0,<0.5.0",
9
9
  "typer[all]>=0.12.0",
10
10
  "httpx>=0.27.0",
11
11
  "pyyaml>=6.0.1",
@@ -0,0 +1,273 @@
1
+ """
2
+ API key management commands.
3
+
4
+ Available as both:
5
+ flowstash api-keys <cmd> (top-level shortcut)
6
+ flowstash project apikey <cmd> (legacy / project-scoped path)
7
+
8
+ Commands:
9
+ new — Create a new API key
10
+ list — List all active API keys
11
+ revoke — Revoke an API key
12
+ """
13
+
14
+ from typing import Optional
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ import asyncio
18
+ import re
19
+
20
+ import typer
21
+ import questionary
22
+ from rich.console import Console
23
+ from rich.prompt import Prompt
24
+ from rich.table import Table
25
+
26
+ from ..core.api_client import APIClient
27
+ from ..core.config import get_access_token
28
+
29
+ app = typer.Typer(
30
+ name="api-keys",
31
+ help="Manage observability API keys",
32
+ no_args_is_help=True,
33
+ )
34
+ console = Console()
35
+
36
+ _VALID_SCOPES = {"observability:ingest", "admin"}
37
+ _SCOPE_DESCRIPTIONS = {
38
+ "observability:ingest": "Write-only ingestion (SDK / workers)",
39
+ "admin": "Full management access (CI / automation)",
40
+ }
41
+
42
+
43
+ def _format_ts(ts) -> str:
44
+ if not ts:
45
+ return "—"
46
+ try:
47
+ return datetime.utcfromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M UTC")
48
+ except Exception:
49
+ return str(ts)
50
+
51
+
52
+ def _find_project_root(start_path: Path = Path.cwd()) -> Optional[Path]:
53
+ for parent in [start_path] + list(start_path.parents):
54
+ if (parent / ".flowstash").exists() or (parent / "pyproject.toml").exists():
55
+ return parent
56
+ return None
57
+
58
+
59
+ def _write_key_to_env(env_name: str, raw_key: str) -> None:
60
+ """Write FLOWSTASH_API_KEY to the environment's .env file."""
61
+ root = _find_project_root()
62
+ if not root:
63
+ console.print(
64
+ "[yellow]Could not find project root — skipping .env update.[/yellow]"
65
+ )
66
+ return
67
+
68
+ candidates = [
69
+ root / "_config" / env_name / ".env",
70
+ root / env_name / ".env",
71
+ ]
72
+ env_file = next((p for p in candidates if p.exists()), None)
73
+
74
+ if not env_file:
75
+ env_dir = root / env_name
76
+ if env_dir.is_dir():
77
+ found = list(env_dir.glob("**/.env"))
78
+ if found:
79
+ env_file = found[0]
80
+
81
+ if not env_file:
82
+ console.print(
83
+ f"[yellow]Could not find .env for environment '{env_name}'. "
84
+ "Set FLOWSTASH_API_KEY manually.[/yellow]"
85
+ )
86
+ return
87
+
88
+ content = env_file.read_text()
89
+ new_line = f"FLOWSTASH_API_KEY={raw_key}"
90
+
91
+ if "FLOWSTASH_API_KEY=" in content:
92
+ content = re.sub(r"#?\s*FLOWSTASH_API_KEY=.*", new_line, content)
93
+ else:
94
+ content = content.rstrip("\n") + f"\n{new_line}\n"
95
+
96
+ env_file.write_text(content)
97
+ console.print(
98
+ f"[green]Written FLOWSTASH_API_KEY to {env_file.relative_to(root)}[/green]"
99
+ )
100
+
101
+
102
+ def _create_key(
103
+ label: Optional[str],
104
+ scope: Optional[str],
105
+ env: Optional[str],
106
+ ):
107
+ """Shared implementation for 'new' and 'create'."""
108
+ token = get_access_token()
109
+ if not token:
110
+ console.print("[red]Not logged in. Run 'flowstash login' first.[/red]")
111
+ raise typer.Exit(code=1)
112
+
113
+ if not label:
114
+ label = Prompt.ask("Key label", default="observability key")
115
+
116
+ if not scope:
117
+ scope = questionary.select(
118
+ "Key scope:",
119
+ choices=[
120
+ questionary.Choice(f"{s} — {_SCOPE_DESCRIPTIONS[s]}", s)
121
+ for s in _VALID_SCOPES
122
+ ],
123
+ default="observability:ingest",
124
+ ).ask()
125
+ if not scope:
126
+ raise typer.Exit(code=1)
127
+
128
+ if scope not in _VALID_SCOPES:
129
+ console.print(
130
+ f"[red]Invalid scope '{scope}'. Valid: {', '.join(_VALID_SCOPES)}[/red]"
131
+ )
132
+ raise typer.Exit(code=1)
133
+
134
+ if scope == "admin" and env:
135
+ console.print(
136
+ "[yellow]Warning: writing an 'admin' key to .env is not recommended. "
137
+ "Admin keys grant full management access — use them in secure CI environments only.[/yellow]"
138
+ )
139
+
140
+ async def _create():
141
+ api = APIClient()
142
+ return await api.post("/v1/api-keys", json={"label": label, "scopes": [scope]})
143
+
144
+ try:
145
+ result = asyncio.run(_create())
146
+ except Exception as e:
147
+ console.print(f"[red]Failed to create API key: {e}[/red]")
148
+ raise typer.Exit(code=1)
149
+
150
+ raw_key = result["api_key"]
151
+
152
+ console.print()
153
+ console.print("[green]✅ API key created![/green]")
154
+ console.print(f" Key ID : [bold]{result['key_id']}[/bold]")
155
+ console.print(f" Label : {label}")
156
+ console.print(f" Scope : {scope}")
157
+ console.print()
158
+ console.print(
159
+ "[bold yellow]⚠ Save this key — it will NOT be shown again:[/bold yellow]"
160
+ )
161
+ console.print(f"\n [bold cyan]{raw_key}[/bold cyan]\n")
162
+
163
+ if env:
164
+ _write_key_to_env(env, raw_key)
165
+
166
+
167
+ @app.command("new")
168
+ def apikey_new(
169
+ label: Optional[str] = typer.Option(
170
+ None, "--label", "-l", help="Human-readable label for the key"
171
+ ),
172
+ scope: Optional[str] = typer.Option(
173
+ None, "--scope", "-s", help="Scope: observability:ingest | admin"
174
+ ),
175
+ env: Optional[str] = typer.Option(
176
+ None, "--env", "-e", help="Write key to this environment's .env file"
177
+ ),
178
+ ):
179
+ """Create a new API key and optionally write it to an environment .env file."""
180
+ _create_key(label=label, scope=scope, env=env)
181
+
182
+
183
+ @app.command("create")
184
+ def apikey_create(
185
+ label: Optional[str] = typer.Option(
186
+ None, "--label", "-l", help="Human-readable label for the key"
187
+ ),
188
+ scope: Optional[str] = typer.Option(
189
+ None, "--scope", "-s", help="Scope: observability:ingest | admin"
190
+ ),
191
+ env: Optional[str] = typer.Option(
192
+ None, "--env", "-e", help="Write key to this environment's .env file"
193
+ ),
194
+ ):
195
+ """Create a new API key (alias for 'new')."""
196
+ _create_key(label=label, scope=scope, env=env)
197
+
198
+
199
+ @app.command("list")
200
+ def apikey_list():
201
+ """List all active API keys for the current tenant."""
202
+ token = get_access_token()
203
+ if not token:
204
+ console.print("[red]Not logged in. Run 'flowstash login' first.[/red]")
205
+ raise typer.Exit(code=1)
206
+
207
+ async def _list():
208
+ api = APIClient()
209
+ return await api.get("/v1/api-keys")
210
+
211
+ try:
212
+ result = asyncio.run(_list())
213
+ except Exception as e:
214
+ console.print(f"[red]Failed to list API keys: {e}[/red]")
215
+ raise typer.Exit(code=1)
216
+
217
+ keys = result.get("api_keys", [])
218
+ if not keys:
219
+ console.print("[dim]No active API keys found.[/dim]")
220
+ return
221
+
222
+ table = Table(title="API Keys", show_lines=True)
223
+ table.add_column("Key ID", style="bold")
224
+ table.add_column("Label")
225
+ table.add_column("Prefix")
226
+ table.add_column("Scopes", style="cyan")
227
+ table.add_column("Created")
228
+ table.add_column("Last Used")
229
+
230
+ for k in keys:
231
+ table.add_row(
232
+ k["key_id"],
233
+ k["label"],
234
+ k["key_prefix"],
235
+ ", ".join(k.get("scopes", [])),
236
+ _format_ts(k.get("created_at")),
237
+ _format_ts(k.get("last_used_at")),
238
+ )
239
+
240
+ console.print(table)
241
+
242
+
243
+ @app.command("revoke")
244
+ def apikey_revoke(
245
+ key_id: str = typer.Argument(..., help="Key ID to revoke (e.g. key-a3b2c1d4)"),
246
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
247
+ ):
248
+ """Revoke an API key immediately."""
249
+ token = get_access_token()
250
+ if not token:
251
+ console.print("[red]Not logged in. Run 'flowstash login' first.[/red]")
252
+ raise typer.Exit(code=1)
253
+
254
+ if not yes:
255
+ from rich.prompt import Confirm
256
+
257
+ if not Confirm.ask(
258
+ f"Revoke key [bold red]{key_id}[/bold red]? This cannot be undone."
259
+ ):
260
+ console.print("Cancelled.")
261
+ return
262
+
263
+ async def _revoke():
264
+ api = APIClient()
265
+ return await api.delete(f"/v1/api-keys/{key_id}")
266
+
267
+ try:
268
+ asyncio.run(_revoke())
269
+ except Exception as e:
270
+ console.print(f"[red]Failed to revoke API key: {e}[/red]")
271
+ raise typer.Exit(code=1)
272
+
273
+ console.print(f"[green]Key {key_id} revoked successfully.[/green]")
@@ -14,26 +14,34 @@ app = typer.Typer()
14
14
  console = Console()
15
15
 
16
16
  from .project import find_project_root
17
- from ..core.docker_utils import check_docker_binary, check_docker_daemon, get_docker_compose_cmd
17
+ from ..core.docker_utils import (
18
+ check_docker_binary,
19
+ check_docker_daemon,
20
+ get_docker_compose_cmd,
21
+ )
22
+
18
23
 
19
24
  async def run_managed_build(tag: str = "latest"):
20
25
  project_config = load_project_config()
21
26
  if not project_config or not project_config.project_id:
22
- console.print("[red]Project not linked. Run 'flowstash init' to link to a managed project.[/red]")
27
+ console.print(
28
+ "[red]Project not linked. Run 'flowstash init' to link to a managed project.[/red]"
29
+ )
23
30
  raise typer.Exit(code=1)
24
-
31
+
25
32
  api = APIClient()
26
33
  # ... rest of existing managed build logic ...
27
34
  # (I'll keep the existing implementation but wrap it)
28
35
 
36
+
29
37
  @app.command()
30
38
  def build(
31
39
  env: str = typer.Argument(..., help="Environment to build"),
32
- tag: str = typer.Option("latest", "--tag", "-t", help="Tag for the image")
40
+ tag: str = typer.Option("latest", "--tag", "-t", help="Tag for the image"),
33
41
  ):
34
42
  """Build project artifacts/images for the specified environment."""
35
43
  project_config = load_project_config()
36
-
44
+
37
45
  # Check if env is managed
38
46
  is_managed = False
39
47
  if project_config:
@@ -41,7 +49,7 @@ def build(
41
49
  if em.name == env:
42
50
  is_managed = em.managed
43
51
  break
44
-
52
+
45
53
  if is_managed:
46
54
  result = asyncio.run(run_build_flow(tag))
47
55
  console.print(f"[green]Managed build completed successfully![/green]")
@@ -59,7 +67,9 @@ def build(
59
67
 
60
68
  compose_file = root / "deployment" / env / "docker-compose.yaml"
61
69
  if not compose_file.exists():
62
- console.print(f"[red]No docker-compose.yaml found for env '{env}' at {compose_file}[/red]")
70
+ console.print(
71
+ f"[red]No docker-compose.yaml found for env '{env}' at {compose_file}[/red]"
72
+ )
63
73
  raise typer.Exit(code=1)
64
74
 
65
75
  cmd = get_docker_compose_cmd() + ["-f", str(compose_file), "build"]
@@ -71,12 +81,13 @@ def build(
71
81
  console.print("[red]Local build failed.[/red]")
72
82
  raise typer.Exit(code=1)
73
83
 
84
+
74
85
  async def run_build_flow(tag: str = "latest"):
75
86
  # (Moved existing run_build_flow logic here for completeness in the file)
76
87
  project_config = load_project_config()
77
88
  project_id = project_config.project_id
78
89
  api = APIClient()
79
-
90
+
80
91
  try:
81
92
  with Progress(
82
93
  SpinnerColumn(),
@@ -85,26 +96,35 @@ async def run_build_flow(tag: str = "latest"):
85
96
  task = progress.add_task(description="Bundling source code...", total=None)
86
97
  tar_path = bundle_source(Path.cwd())
87
98
  progress.update(task, description="Source bundled.")
88
-
89
- task = progress.add_task(description="Requesting upload path...", total=None)
90
- upload_data = await api.get("/v1/builds/upload-path")
99
+
100
+ task = progress.add_task(
101
+ description="Requesting upload path...", total=None
102
+ )
103
+ upload_data = await api.get(
104
+ "/v1/builds/upload-path?project_id=" + project_id
105
+ )
91
106
  build_id = upload_data["build_id"]
92
107
  upload_url = upload_data["upload_url"]
93
108
  progress.update(task, description="Upload path received.")
94
-
109
+
95
110
  task = progress.add_task(description="Uploading source...", total=None)
96
111
  with open(tar_path, "rb") as f:
97
112
  data = f.read()
98
- await api.put_binary(upload_url, data, headers={"Content-Type": "application/gzip"})
113
+ await api.put_binary(
114
+ upload_url, data, headers={"Content-Type": "application/gzip"}
115
+ )
99
116
  progress.update(task, description="Source uploaded.")
100
-
117
+
101
118
  task = progress.add_task(description="Triggering build...", total=None)
102
- trigger_resp = await api.post(f"/v1/builds/{build_id}/trigger", json={"image_tag": tag})
103
-
119
+ trigger_resp = await api.post(
120
+ f"/v1/builds/{build_id}/trigger",
121
+ json={"image_tag": tag, "project_id": project_id},
122
+ )
123
+
104
124
  # update build_id to the triggered true GCP build ID
105
125
  build_id = trigger_resp["build_id"]
106
126
  progress.update(task, description=f"Build triggered (ID: {build_id}).")
107
-
127
+
108
128
  task = progress.add_task(description="Building...", total=None)
109
129
  while True:
110
130
  status_data = await api.get(f"/v1/builds/{build_id}/status")
@@ -113,11 +133,15 @@ async def run_build_flow(tag: str = "latest"):
113
133
  progress.update(task, description="Build successful!")
114
134
  return status_data
115
135
  elif status in ["FAILURE", "INTERNAL_ERROR", "TIMEOUT", "CANCELLED"]:
116
- progress.update(task, description=f"[red]Build failed: {status}[/red]")
136
+ progress.update(
137
+ task, description=f"[red]Build failed: {status}[/red]"
138
+ )
117
139
  console.print(f"[red]Build ended with status: {status}[/red]")
118
- log_url = status_data.get('log_url')
140
+ log_url = status_data.get("log_url")
119
141
  if log_url:
120
- console.print(f"Check logs here: [link={log_url}]{log_url}[/link]")
142
+ console.print(
143
+ f"Check logs here: [link={log_url}]{log_url}[/link]"
144
+ )
121
145
  raise typer.Exit(code=1)
122
146
  await asyncio.sleep(5)
123
147
  except Exception as e: