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.
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/PKG-INFO +2 -2
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/pyproject.toml +2 -2
- flowstash_cli-0.3.0/src/flowstash/cli/commands/apikey.py +273 -0
- flowstash_cli-0.3.0/src/flowstash/cli/commands/client.py +479 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/project.py +26 -217
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/main.py +22 -1
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/AGENTS.md +34 -0
- flowstash_cli-0.3.0/src/flowstash/cli/templates/_config/shared/backend.yaml +80 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/shared/clients/demoClient.yaml +16 -1
- flowstash_cli-0.2.12/src/flowstash/cli/templates/_config/shared/backend.yaml +0 -7
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/__init__.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/__init__.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/auth.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/build.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/deploy.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/run.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/commands/webhook.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/__init__.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/api_client.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/auth_server.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/builder.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/config.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/docker_utils.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/core/patcher.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/README.md +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_.dockerignore +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_.flowstash +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_api_main.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(backend-asyncio)/backend.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(backend-dramatiq)/backend.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(backend-managed)/backend.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(observability-logfile)/observability.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/(observability-managed)/observability.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/.env +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/[env]/_backend.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/shared/.env +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_config/shared/clients.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/shared/.env +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/shared/api.Dockerfile +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_deployment/shared/worker.Dockerfile +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_pyproject.toml +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_api/__init__.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_api/_routes/webhooks.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_shared/__init__.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_shared/clients/client.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_shared/models/models.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_shared/tasks/sharedTasks.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_worker/__init__.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_src/_worker/tasks/tasks.py +0 -0
- {flowstash_cli-0.2.12 → flowstash_cli-0.3.0}/src/flowstash/cli/templates/_worker_main.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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]")
|