flowstash-cli 0.2.12__tar.gz → 0.3.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.12 → flowstash_cli-0.3.0}/PKG-INFO +2 -2
  2. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/pyproject.toml +2 -2
  3. flowstash_cli-0.3.0/src/flowstash/cli/commands/apikey.py +273 -0
  4. flowstash_cli-0.3.0/src/flowstash/cli/commands/client.py +479 -0
  5. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/project.py +26 -217
  6. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/main.py +22 -1
  7. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/AGENTS.md +34 -0
  8. flowstash_cli-0.3.0/src/flowstash/cli/templates/_config/shared/backend.yaml +80 -0
  9. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/shared/clients/demoClient.yaml +16 -1
  10. flowstash_cli-0.2.12/src/flowstash/cli/templates/_config/shared/backend.yaml +0 -7
  11. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/__init__.py +0 -0
  12. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/__init__.py +0 -0
  13. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/auth.py +0 -0
  14. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/build.py +0 -0
  15. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/deploy.py +0 -0
  16. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/run.py +0 -0
  17. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/webhook.py +0 -0
  18. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/__init__.py +0 -0
  19. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/api_client.py +0 -0
  20. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/auth_server.py +0 -0
  21. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/builder.py +0 -0
  22. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/config.py +0 -0
  23. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/docker_utils.py +0 -0
  24. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/patcher.py +0 -0
  25. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/README.md +0 -0
  26. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_.dockerignore +0 -0
  27. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_.flowstash +0 -0
  28. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_api_main.py +0 -0
  29. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(backend-asyncio)/backend.yaml +0 -0
  30. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(backend-dramatiq)/backend.yaml +0 -0
  31. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(backend-managed)/backend.yaml +0 -0
  32. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(observability-logfile)/observability.yaml +0 -0
  33. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(observability-managed)/observability.yaml +0 -0
  34. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/.env +0 -0
  35. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/_backend.yaml +0 -0
  36. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/shared/.env +0 -0
  37. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/shared/clients.yaml +0 -0
  38. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +0 -0
  39. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +0 -0
  40. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/shared/.env +0 -0
  41. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/shared/api.Dockerfile +0 -0
  42. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/shared/worker.Dockerfile +0 -0
  43. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_pyproject.toml +0 -0
  44. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_api/__init__.py +0 -0
  45. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_api/_routes/webhooks.py +0 -0
  46. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_shared/__init__.py +0 -0
  47. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_shared/clients/client.py +0 -0
  48. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_shared/models/models.py +0 -0
  49. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_shared/tasks/sharedTasks.py +0 -0
  50. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_worker/__init__.py +0 -0
  51. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_worker/tasks/tasks.py +0 -0
  52. {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_worker_main.py +0 -0
  53. {flowstash_cli-0.2.12 → flowstash_cli-0.3.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.12
3
+ Version: 0.3.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.12,<0.3.0)
12
+ Requires-Dist: flowstash-runtime (>=0.3.0,<0.4.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.12"
3
+ version = "0.3.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.12,<0.3.0",
8
+ "flowstash-runtime>=0.3.0,<0.4.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]")