flowstash-cli 0.6.4__tar.gz → 0.7.1__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 (48) hide show
  1. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/PKG-INFO +2 -2
  2. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/pyproject.toml +2 -2
  3. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/auth.py +64 -25
  4. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/deploy.py +3 -1
  5. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/webhook.py +35 -12
  6. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/main.py +6 -0
  7. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_deployment/shared/api.Dockerfile +2 -2
  8. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_deployment/shared/worker.Dockerfile +1 -1
  9. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/__init__.py +0 -0
  10. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/__init__.py +0 -0
  11. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/apikey.py +0 -0
  12. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/build.py +0 -0
  13. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/client.py +0 -0
  14. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/project.py +0 -0
  15. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/commands/run.py +0 -0
  16. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/core/__init__.py +0 -0
  17. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/core/api_client.py +0 -0
  18. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/core/auth_server.py +0 -0
  19. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/core/builder.py +0 -0
  20. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/core/config.py +0 -0
  21. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/core/docker_utils.py +0 -0
  22. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/core/patcher.py +0 -0
  23. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/AGENTS.md +0 -0
  24. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/README.md +0 -0
  25. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_.dockerignore +0 -0
  26. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_.flowstash +0 -0
  27. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_api_main.py +0 -0
  28. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_config/[env]/(backend-asyncio)/backend.yaml +0 -0
  29. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_config/[env]/(backend-dramatiq)/backend.yaml +0 -0
  30. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_config/[env]/(backend-managed)/backend.yaml +0 -0
  31. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_config/[env]/(observability-logfile)/observability.yaml +0 -0
  32. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_config/[env]/(observability-managed)/observability.yaml +0 -0
  33. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_config/shared/backend.yaml +0 -0
  34. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_config/shared/clients/demoClient.yaml +0 -0
  35. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_config/shared/clients.yaml +0 -0
  36. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +0 -0
  37. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +0 -0
  38. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_pyproject.toml +0 -0
  39. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_src/_api/__init__.py +0 -0
  40. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_src/_api/_routes/webhooks.py +0 -0
  41. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_src/_shared/__init__.py +0 -0
  42. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_src/_shared/clients/client.py +0 -0
  43. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_src/_shared/models/models.py +0 -0
  44. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_src/_shared/tasks/sharedTasks.py +0 -0
  45. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_src/_worker/__init__.py +0 -0
  46. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_src/_worker/tasks/tasks.py +0 -0
  47. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/src/flowstash/cli/templates/_worker_main.py +0 -0
  48. {flowstash_cli-0.6.4 → flowstash_cli-0.7.1}/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.6.4
3
+ Version: 0.7.1
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.6.4,<0.7.0)
12
+ Requires-Dist: flowstash-runtime (>=0.7.1,<0.8.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.6.4"
3
+ version = "0.7.1"
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.6.4,<0.7.0",
8
+ "flowstash-runtime>=0.7.1,<0.8.0",
9
9
  "typer[all]>=0.12.0",
10
10
  "httpx>=0.27.0",
11
11
  "pyyaml>=6.0.1",
@@ -7,35 +7,42 @@ from rich.console import Console
7
7
  import asyncio
8
8
  import httpx
9
9
  from ..core.auth_server import start_callback_server
10
+ from ..core.api_client import APIClient
10
11
  from ..core.config import (
11
- load_global_config,
12
- set_access_token,
13
- get_access_token,
12
+ load_global_config,
13
+ set_access_token,
14
+ get_access_token,
14
15
  delete_access_token,
15
- load_project_config
16
+ load_project_config,
16
17
  )
17
18
 
18
19
  app = typer.Typer()
19
20
  console = Console()
20
21
  import os
22
+
21
23
  API_URL = os.getenv("FLOWSTASH_API_URL", "https://api.flowstash.dev")
22
24
 
25
+
23
26
  @app.command()
24
27
  def login(
25
- username: Optional[str] = typer.Option(None, "--username", "-u", help="Username for manual login"),
26
- password: Optional[str] = typer.Option(None, "--password", "-p", help="Password for manual login")
28
+ username: Optional[str] = typer.Option(
29
+ None, "--username", "-u", help="Username for manual login"
30
+ ),
31
+ password: Optional[str] = typer.Option(
32
+ None, "--password", "-p", help="Password for manual login"
33
+ ),
27
34
  ):
28
35
  """Log in to the flowstash Managed Platform."""
29
36
  if username and password:
30
37
  # Manual login
31
38
  console.print(f"Logging in to {API_URL} as {username}...")
32
-
39
+
33
40
  async def do_login():
34
41
  async with httpx.AsyncClient() as client:
35
42
  try:
36
43
  resp = await client.post(
37
44
  f"{API_URL}/v1/auth/login",
38
- json={"email": username, "password": password}
45
+ json={"email": username, "password": password},
39
46
  )
40
47
  resp.raise_for_status()
41
48
  return resp.json()
@@ -46,10 +53,10 @@ def login(
46
53
  result = asyncio.run(do_login())
47
54
  if not result:
48
55
  raise typer.Exit(code=1)
49
-
56
+
50
57
  access_token = result.get("access_token")
51
58
  tenant_id = result.get("tenant_id")
52
-
59
+
53
60
  set_access_token(access_token)
54
61
  console.print(f"[green]Successfully logged in as tenant: {tenant_id}[/green]")
55
62
  return
@@ -58,55 +65,87 @@ def login(
58
65
  state = secrets.token_urlsafe(16)
59
66
  port = 8888
60
67
  callback_url = f"http://localhost:{port}/callback"
61
-
68
+
62
69
  login_url = f"{API_URL}/cli-login?redirect_uri={callback_url}&state={state}"
63
-
70
+
64
71
  console.print(f"Opening your browser to authenticate...")
65
- console.print(f"If the browser doesn't open, visit: [link={login_url}]{login_url}[/link]")
66
-
72
+ console.print(
73
+ f"If the browser doesn't open, visit: [link={login_url}]{login_url}[/link]"
74
+ )
75
+
67
76
  webbrowser.open(login_url)
68
-
77
+
69
78
  console.print("Waiting for authentication...")
70
79
  result = start_callback_server(port)
71
-
80
+
72
81
  if not result:
73
82
  console.print("[red]Authentication timed out or failed.[/red]")
74
83
  raise typer.Exit(code=1)
75
-
84
+
76
85
  if result.get("state") != state:
77
- console.print("[red]Invalid state received. Auth session might be compromised.[/red]")
86
+ console.print(
87
+ "[red]Invalid state received. Auth session might be compromised.[/red]"
88
+ )
78
89
  raise typer.Exit(code=1)
79
-
90
+
80
91
  access_token = result.get("access_token")
81
92
  tenant_id = result.get("tenant_id")
82
-
93
+
83
94
  if not access_token:
84
95
  console.print("[red]No access token received.[/red]")
85
96
  raise typer.Exit(code=1)
86
-
97
+
87
98
  set_access_token(access_token)
88
-
99
+
89
100
  console.print(f"[green]Successfully logged in as tenant: {tenant_id}[/green]")
90
101
 
102
+
91
103
  @app.command()
92
104
  def logout():
93
105
  """Log out from the flowstash Managed Platform."""
94
106
  delete_access_token()
95
107
  console.print("[yellow]Logged out successfully.[/yellow]")
96
108
 
109
+
110
+ def _fetch_current_user() -> Optional[dict]:
111
+ try:
112
+ return asyncio.run(APIClient().get("/v1/auth/me"))
113
+ except Exception:
114
+ return None
115
+
116
+
97
117
  @app.command()
98
118
  def whoami():
99
119
  """Show current login status."""
100
120
  token = get_access_token()
101
121
  global_config = load_global_config()
102
122
  project_config = load_project_config()
103
-
123
+
104
124
  if token:
105
125
  console.print(f"API URL: {global_config.api_url}")
126
+ user_info = _fetch_current_user()
127
+ login = None
128
+ tenant_id = None
129
+
130
+ if user_info:
131
+ login = (
132
+ user_info.get("email")
133
+ or user_info.get("username")
134
+ or user_info.get("login")
135
+ )
136
+ tenant_id = user_info.get("tenant_id")
137
+ elif project_config:
138
+ login = project_config.user_email
139
+ tenant_id = project_config.tenant_id
140
+
141
+ if login:
142
+ console.print(f"Logged in as: [bold]{login}[/bold]")
143
+ if tenant_id:
144
+ console.print(f"Tenant: [bold]{tenant_id}[/bold]")
145
+
106
146
  if project_config:
107
- console.print(f"Logged in as tenant: [bold]{project_config.tenant_id}[/bold]")
108
147
  console.print(f"Current Project: [bold]{project_config.project_id}[/bold]")
109
- else:
148
+ elif not user_info:
110
149
  console.print("Logged in, but no project context found in this directory.")
111
150
  else:
112
151
  console.print("Not logged in.")
@@ -7,6 +7,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
7
7
  from ..core.api_client import APIClient
8
8
  from ..core.config import load_project_config
9
9
  from .build import run_build_flow
10
+ import flowstash.runtime
10
11
 
11
12
  app = typer.Typer()
12
13
  console = Console()
@@ -17,7 +18,7 @@ _STATUS_LABELS = {
17
18
  "VALIDATING": "Validating container images...",
18
19
  "DEPLOYING": "Deploying services...",
19
20
  "HEALTH_CHECK": "Health-checking API and Worker...",
20
- "SYNCING_SCHEDULES": "Fetching and syncing scheduled tasks...",
21
+ "SYNCING_SCHEDULES": "Waiting for deployment verification...",
21
22
  "DEPLOYED": "Deployed successfully ✓",
22
23
  "FAILED": "Deployment failed.",
23
24
  }
@@ -60,6 +61,7 @@ async def run_deploy_flow(env: str, artifact_id: Optional[str] = None):
60
61
  "project_id": project_id,
61
62
  "artifact_id": artifact_id,
62
63
  "env_vars": {"ENVIRONMENT": env},
64
+ "flowstash_runtime_version": flowstash.runtime.__version__,
63
65
  },
64
66
  )
65
67
  deploy_id = deploy_data["deploy_id"]
@@ -36,6 +36,10 @@ def get_api_url() -> str:
36
36
  )
37
37
 
38
38
 
39
+ def _build_target_url(target: str, path: str) -> str:
40
+ return f"{target.rstrip('/')}/{path.lstrip('/')}"
41
+
42
+
39
43
  def _get_webhooks_from_app(entry: str, debug: bool = False) -> List[Dict[str, Any]]:
40
44
  """Loads the app entrypoint in a subprocess and extracts registered webhooks."""
41
45
 
@@ -168,7 +172,10 @@ print(json.dumps({{"webhooks": result}}))
168
172
 
169
173
 
170
174
  async def _listen_stream(
171
- ws_url: str, captures_buffer: List[Dict[str, Any]], stop_event: asyncio.Event
175
+ ws_url: str,
176
+ captures_buffer: List[Dict[str, Any]],
177
+ stop_event: asyncio.Event,
178
+ error_ref: Optional[List[str]] = None,
172
179
  ):
173
180
  """Connects to the WebSocket and appends captures to the buffer."""
174
181
 
@@ -189,10 +196,14 @@ async def _listen_stream(
189
196
  except asyncio.TimeoutError:
190
197
  continue
191
198
  except websockets.exceptions.ConnectionClosed:
192
- console.print("[yellow]Connection closed by server.[/yellow]")
199
+ if error_ref is not None:
200
+ error_ref.append("Connection closed by server.")
201
+ stop_event.set()
193
202
  break
194
203
  except Exception as e:
195
- console.print(f"[red]WebSocket connection error:[/red] {e}")
204
+ if error_ref is not None:
205
+ error_ref.append(str(e))
206
+ stop_event.set()
196
207
 
197
208
 
198
209
  async def _wait_for_keypress(stop_event: asyncio.Event):
@@ -205,9 +216,12 @@ async def _wait_for_keypress(stop_event: asyncio.Event):
205
216
  async def _run_listen_ui(ws_url: str, path: str, ingest_url: str):
206
217
  captures_buffer: List[Dict[str, Any]] = []
207
218
  stop_event = asyncio.Event()
219
+ ws_error: List[str] = []
208
220
 
209
221
  # Start WS listener task
210
- ws_task = asyncio.create_task(_listen_stream(ws_url, captures_buffer, stop_event))
222
+ ws_task = asyncio.create_task(
223
+ _listen_stream(ws_url, captures_buffer, stop_event, ws_error)
224
+ )
211
225
  input_task = asyncio.create_task(_wait_for_keypress(stop_event))
212
226
 
213
227
  console.print(f"[green]Listening on[/green] [bold]{ingest_url}[/bold]")
@@ -271,6 +285,11 @@ async def _run_listen_ui(ws_url: str, path: str, ingest_url: str):
271
285
 
272
286
  # Wait for tasks to clean up
273
287
  ws_task.cancel()
288
+ input_task.cancel()
289
+
290
+ if ws_error:
291
+ console.print(f"[red]WebSocket connection error:[/red] {ws_error[0]}")
292
+ return []
274
293
 
275
294
  return captures_buffer
276
295
 
@@ -552,6 +571,7 @@ def test(
552
571
  raise typer.Exit(code=0)
553
572
 
554
573
  actual_path = path
574
+ prompt_for_target_url = False
555
575
  selected_webhook = None
556
576
 
557
577
  if actual_path:
@@ -578,17 +598,13 @@ def test(
578
598
  if not webhook_path:
579
599
  raise typer.Exit(code=0)
580
600
 
601
+ prompt_for_target_url = True
581
602
  selected_webhook = next(
582
603
  (w for w in webhooks if w["path"] == webhook_path), None
583
604
  )
584
605
 
585
606
  if not actual_path:
586
- actual_path = questionary.text(
587
- "Target URL path (modify if needed for router prefixes):",
588
- default=webhook_path,
589
- ).ask()
590
- if not actual_path:
591
- raise typer.Exit(code=0)
607
+ actual_path = webhook_path
592
608
 
593
609
  test_payload_path = selected_webhook.get("test_payload_path")
594
610
  if not test_payload_path:
@@ -624,8 +640,15 @@ def test(
624
640
  if k.lower() not in ("content-length", "host", "connection")
625
641
  }
626
642
 
627
- # Optional logic for target_url
628
- target_url = target.rstrip("/") + "/" + actual_path.lstrip("/")
643
+ target_url = _build_target_url(target, actual_path)
644
+
645
+ if prompt_for_target_url:
646
+ target_url = questionary.text(
647
+ "Target URL to call (modify base URL, port, or path):",
648
+ default=target_url,
649
+ ).ask()
650
+ if not target_url:
651
+ raise typer.Exit(code=0)
629
652
 
630
653
  console.print(f"[cyan]Testing {method} {target_url}...[/cyan]")
631
654
 
@@ -356,6 +356,12 @@ def whoami():
356
356
  auth_cmds.whoami()
357
357
 
358
358
 
359
+ @app.command("logged-in")
360
+ def logged_in():
361
+ """Show who is currently logged in."""
362
+ auth_cmds.whoami()
363
+
364
+
359
365
  @app.command()
360
366
  def logout():
361
367
  """Log out from the platform."""
@@ -11,8 +11,8 @@ ARG SERVICE_EXTRAS="api"
11
11
  # Install base deps + extras into system site-packages
12
12
  # Base deps:
13
13
  RUN uv pip install --system -r pyproject.toml
14
- RUN uv pip install --system -r pyproject.toml --extra api || echo "no worker extra; skipping"
14
+ RUN uv pip install --system -r pyproject.toml --extra api || echo "no api extra; skipping"
15
15
  RUN rm -rf src/worker
16
16
  EXPOSE 8000
17
17
  ENV PYTHONPATH=/app/src:/app
18
- CMD ["python", "api_main.py"]
18
+ ENTRYPOINT ["python", "api_main.py"]
@@ -15,4 +15,4 @@ RUN uv pip install --system -r pyproject.toml --extra worker || echo "no worker
15
15
 
16
16
  RUN rm -rf src/api
17
17
  ENV PYTHONPATH=/app/src:/app
18
- CMD ["python", "-u", "worker_main.py"]
18
+ ENTRYPOINT ["python", "-u", "worker_main.py"]