flowstash-cli 0.1.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.
Files changed (48) hide show
  1. flowstash/__init__.py +0 -0
  2. flowstash/commands/__init__.py +0 -0
  3. flowstash/commands/auth.py +112 -0
  4. flowstash/commands/build.py +127 -0
  5. flowstash/commands/deploy.py +143 -0
  6. flowstash/commands/project.py +555 -0
  7. flowstash/commands/run.py +65 -0
  8. flowstash/core/__init__.py +0 -0
  9. flowstash/core/api_client.py +62 -0
  10. flowstash/core/auth_server.py +45 -0
  11. flowstash/core/builder.py +40 -0
  12. flowstash/core/config.py +81 -0
  13. flowstash/core/docker_utils.py +33 -0
  14. flowstash/main.py +269 -0
  15. flowstash/templates/AGENTS.md +222 -0
  16. flowstash/templates/README.md +135 -0
  17. flowstash/templates/_.dockerignore +8 -0
  18. flowstash/templates/_.flowstash +4 -0
  19. flowstash/templates/_api_main.py +21 -0
  20. flowstash/templates/_config/[env]/(backend-asyncio)/backend.yaml +4 -0
  21. flowstash/templates/_config/[env]/(backend-dramatiq)/backend.yaml +8 -0
  22. flowstash/templates/_config/[env]/(backend-managed)/backend.yaml +6 -0
  23. flowstash/templates/_config/[env]/_backend.yaml +4 -0
  24. flowstash/templates/_config/shared/.env +1 -0
  25. flowstash/templates/_config/shared/backend.yaml +4 -0
  26. flowstash/templates/_config/shared/clients/demoClient.yaml +39 -0
  27. flowstash/templates/_config/shared/clients.yaml +3 -0
  28. flowstash/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +24 -0
  29. flowstash/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +34 -0
  30. flowstash/templates/_deployment/[env]/.env +3 -0
  31. flowstash/templates/_deployment/shared/.env +5 -0
  32. flowstash/templates/_deployment/shared/api.Dockerfile +18 -0
  33. flowstash/templates/_deployment/shared/worker.Dockerfile +18 -0
  34. flowstash/templates/_pyproject.toml +40 -0
  35. flowstash/templates/_src/_api/__init__.py +1 -0
  36. flowstash/templates/_src/_api/_routes/webhooks.py +25 -0
  37. flowstash/templates/_src/_shared/__init__.py +1 -0
  38. flowstash/templates/_src/_shared/clients/client.py +18 -0
  39. flowstash/templates/_src/_shared/models/models.py +1 -0
  40. flowstash/templates/_src/_shared/tasks/sharedTasks.py +10 -0
  41. flowstash/templates/_src/_worker/__init__.py +1 -0
  42. flowstash/templates/_src/_worker/tasks/tasks.py +15 -0
  43. flowstash/templates/_worker_main.py +25 -0
  44. flowstash/ui/__init__.py +0 -0
  45. flowstash_cli-0.1.0.dist-info/METADATA +19 -0
  46. flowstash_cli-0.1.0.dist-info/RECORD +48 -0
  47. flowstash_cli-0.1.0.dist-info/WHEEL +4 -0
  48. flowstash_cli-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,45 @@
1
+ from typing import Dict, Optional
2
+ import threading
3
+ import queue
4
+ from http.server import HTTPServer, BaseHTTPRequestHandler
5
+ from urllib.parse import urlparse, parse_qs
6
+
7
+ result_queue = queue.Queue()
8
+
9
+ class CallbackHandler(BaseHTTPRequestHandler):
10
+ def do_GET(self):
11
+ query_components = parse_qs(urlparse(self.path).query)
12
+ params = {k: v[0] for k, v in query_components.items()}
13
+ result_queue.put(params)
14
+
15
+ self.send_response(200)
16
+ self.send_header('Content-type', 'text/html')
17
+ self.end_headers()
18
+
19
+ content = """
20
+ <html>
21
+ <body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
22
+ <h1>Authentication Successful</h1>
23
+ <p>You can now close this window and return to the CLI.</p>
24
+ </body>
25
+ </html>
26
+ """
27
+ self.wfile.write(content.encode('utf-8'))
28
+
29
+ def log_message(self, format, *args):
30
+ # Suppress logging
31
+ pass
32
+
33
+ def start_callback_server(port: int = 8888) -> Dict[str, str]:
34
+ server = HTTPServer(('127.0.0.1', port), CallbackHandler)
35
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
36
+ thread.start()
37
+
38
+ # Wait for result
39
+ try:
40
+ result = result_queue.get(timeout=300) # 5 minutes timeout
41
+ server.shutdown()
42
+ return result
43
+ except queue.Empty:
44
+ server.shutdown()
45
+ return {}
@@ -0,0 +1,40 @@
1
+ import os
2
+ import tarfile
3
+ import tempfile
4
+ import gzip
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ def bundle_source(directory: Path, ignore_patterns: Optional[List[str]] = None) -> Path:
9
+ """
10
+ Bundles the source code in directory into a .tar.gz file.
11
+ Follows .gitignore if present.
12
+ """
13
+ temp_dir = Path(tempfile.gettempdir())
14
+ tar_path = temp_dir / f"source_{os.urandom(4).hex()}.tar.gz"
15
+
16
+ # Basic ignore logic (should be improved to handle .gitignore properly)
17
+ default_ignore = {".git", ".venv", "__pycache__", ".pytest_cache", ".flowstash"}
18
+ if ignore_patterns:
19
+ default_ignore.update(ignore_patterns)
20
+
21
+ def is_ignored(path: Path) -> bool:
22
+ for part in path.parts:
23
+ if part in default_ignore:
24
+ return True
25
+ return False
26
+
27
+ with tarfile.open(tar_path, "w:gz") as tar:
28
+ for root, dirs, files in os.walk(directory):
29
+ root_path = Path(root)
30
+ if is_ignored(root_path.relative_to(directory)):
31
+ continue
32
+
33
+ for file in files:
34
+ file_path = root_path / file
35
+ if is_ignored(file_path.relative_to(directory)):
36
+ continue
37
+
38
+ tar.add(file_path, arcname=str(file_path.relative_to(directory)))
39
+
40
+ return tar_path
@@ -0,0 +1,81 @@
1
+ from typing import List
2
+ from pydantic import Field
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional, Dict, Any
6
+ import yaml
7
+ import keyring
8
+ from pydantic import BaseModel
9
+
10
+ flowstash_DIR = Path.home() / ".flowstash"
11
+ GLOBAL_CONFIG_FILE = flowstash_DIR / "config.yaml"
12
+ PROJECT_CONFIG_FILE = ".flowstash"
13
+
14
+ SERVICE_NAME = "flowstash"
15
+ TOKEN_KEY = "access_token"
16
+
17
+ class GlobalConfig(BaseModel):
18
+ api_url: str = os.getenv("flowstash_API_URL", "https://api.flowstash.dev") # Default to managed platform
19
+
20
+ class EnvironmentMode(BaseModel):
21
+ name: str
22
+ managed: bool = False
23
+ options: Dict[str, str] = Field(default_factory=dict)
24
+
25
+ class ProjectConfig(BaseModel):
26
+ project_name: Optional[str] = None
27
+ project_id: Optional[str] = None
28
+ tenant_id: Optional[str] = None
29
+ user_email: Optional[str] = None
30
+ environments: List[EnvironmentMode] = Field(default_factory=list)
31
+
32
+ def load_global_config() -> GlobalConfig:
33
+ if not GLOBAL_CONFIG_FILE.exists():
34
+ return GlobalConfig()
35
+
36
+ try:
37
+ with open(GLOBAL_CONFIG_FILE, "r") as f:
38
+ data = yaml.safe_load(f) or {}
39
+ return GlobalConfig(**data)
40
+ except Exception:
41
+ return GlobalConfig()
42
+
43
+ def save_global_config(config: GlobalConfig):
44
+ flowstash_DIR.mkdir(parents=True, exist_ok=True)
45
+ with open(GLOBAL_CONFIG_FILE, "w") as f:
46
+ yaml.safe_dump(config.model_dump(), f)
47
+
48
+ def get_access_token() -> Optional[str]:
49
+ return keyring.get_password(SERVICE_NAME, TOKEN_KEY)
50
+
51
+ def set_access_token(token: str):
52
+ keyring.set_password(SERVICE_NAME, TOKEN_KEY, token)
53
+
54
+ def delete_access_token():
55
+ try:
56
+ keyring.delete_password(SERVICE_NAME, TOKEN_KEY)
57
+ except keyring.errors.PasswordDeleteError:
58
+ pass
59
+
60
+ def load_project_config() -> Optional[ProjectConfig]:
61
+ path = Path(PROJECT_CONFIG_FILE)
62
+ if not path.exists():
63
+ return None
64
+
65
+ try:
66
+ with open(path, "r") as f:
67
+ data = yaml.safe_load(f) or {}
68
+ return ProjectConfig(**data)
69
+ except Exception:
70
+ return None
71
+
72
+ def save_project_config(config: ProjectConfig):
73
+ with open(PROJECT_CONFIG_FILE, "w") as f:
74
+ yaml.safe_dump(config.model_dump(), f)
75
+
76
+ # Legacy aliases for compatibility if needed, though we should update callers
77
+ def load_config() -> GlobalConfig:
78
+ return load_global_config()
79
+
80
+ def save_config(config: GlobalConfig):
81
+ save_global_config(config)
@@ -0,0 +1,33 @@
1
+ import shutil
2
+ import subprocess
3
+ from typing import Tuple, Optional
4
+
5
+ def check_docker_binary() -> bool:
6
+ return shutil.which("docker") is not None
7
+
8
+ def check_docker_compose_binary() -> bool:
9
+ # Check for 'docker compose' (V2) or 'docker-compose' (V1)
10
+ if shutil.which("docker-compose"):
11
+ return True
12
+
13
+ # Check if 'docker compose' works
14
+ try:
15
+ subprocess.run(["docker", "compose", "version"], capture_output=True, check=True)
16
+ return True
17
+ except (subprocess.CalledProcessError, FileNotFoundError):
18
+ return False
19
+
20
+ def check_docker_daemon() -> Tuple[bool, Optional[str]]:
21
+ """Returns (is_running, error_message)"""
22
+ try:
23
+ subprocess.run(["docker", "info"], capture_output=True, check=True)
24
+ return True, None
25
+ except subprocess.CalledProcessError as e:
26
+ return False, e.stderr.decode().strip()
27
+ except FileNotFoundError:
28
+ return False, "Docker binary not found"
29
+
30
+ def get_docker_compose_cmd() -> list:
31
+ if shutil.which("docker-compose"):
32
+ return ["docker-compose"]
33
+ return ["docker", "compose"]
flowstash/main.py ADDED
@@ -0,0 +1,269 @@
1
+ import typer
2
+ import click
3
+ from typing import Optional
4
+ from .commands import auth as auth_cmds
5
+ from .commands import project as project_cmds
6
+ from .commands import build as build_cmds
7
+ from .commands import deploy as deploy_cmds
8
+ from .commands import run as run_cmds
9
+ from rich.console import Console
10
+ import importlib.metadata
11
+
12
+ try:
13
+ __version__ = importlib.metadata.version("flowstash-cli")
14
+ except importlib.metadata.PackageNotFoundError:
15
+ __version__ = "unknown"
16
+
17
+ console = Console()
18
+
19
+ app = typer.Typer(
20
+ name="flowstash",
21
+ help=f"""
22
+ # 🚀 flowstash CLI (v{__version__})
23
+ Manage your flowstash projects and deployments with ease.
24
+
25
+ ## 🛠 Usage
26
+ Run commands below to initialize, build, and deploy your integrations.
27
+ """,
28
+ add_completion=False,
29
+ no_args_is_help=True,
30
+ rich_markup_mode="rich"
31
+ )
32
+
33
+ env_app = typer.Typer(
34
+ name="env",
35
+ help="Manage environments",
36
+ no_args_is_help=True,
37
+ rich_markup_mode="rich"
38
+ )
39
+ app.add_typer(env_app, name="env")
40
+
41
+ @app.command()
42
+ def help(ctx: typer.Context):
43
+ """
44
+ Show help information for all commands.
45
+ """
46
+ console.print(ctx.parent.get_help()) if ctx.parent else console.print(ctx.get_help())
47
+
48
+ # Shortcuts / Top-level commands
49
+ @app.command()
50
+ def login(
51
+ username: Optional[str] = typer.Option(None, "--username", "-u", help="Username for manual login"),
52
+ password: Optional[str] = typer.Option(None, "--password", "-p", help="Password for manual login")
53
+ ):
54
+ """Log in to the flowstash Managed Platform."""
55
+ auth_cmds.login(username=username, password=password)
56
+
57
+ @app.command()
58
+ def init(
59
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Project name"),
60
+ fix: bool = typer.Option(False, "--fix", help="Repair missing components"),
61
+ force: bool = typer.Option(False, "--force", help="Force overwrite existing files")
62
+ ):
63
+ """
64
+ [bold green]Initialize[/bold green] or [bold yellow]repair[/bold yellow] a flowstash project.
65
+
66
+ This command sets up the local structure, creates the `.flowstash` config,
67
+ and lets you configure your first environments.
68
+ """
69
+ project_cmds.init(name=name, fix=fix, force=force)
70
+
71
+ @app.command()
72
+ def check():
73
+ """Inspect the current project structure."""
74
+ project_cmds.check()
75
+
76
+ @app.command()
77
+ def link():
78
+ """
79
+ [bold cyan]Link[/bold cyan] your current project to a flowstash Managed Project.
80
+ """
81
+ from .core.config import load_project_config
82
+ from .commands.project import _link_project
83
+
84
+ project_config = load_project_config()
85
+ if not project_config:
86
+ console.print("[red]No .flowstash found. Please run 'flowstash init' first.[/red]")
87
+ raise typer.Exit(code=1)
88
+
89
+ _link_project(project_config)
90
+
91
+ @env_app.command("add")
92
+ def env_add(
93
+ env_name: Optional[str] = typer.Argument(None, help="Environment name to add"),
94
+ force: bool = typer.Option(False, "--force", help="Force overwrite existing files")
95
+ ):
96
+ """
97
+ [bold blue]Add a new environment[/bold blue] to your project.
98
+
99
+ Choose between Local, Dev, Prod, or Custom. It will automatically filter
100
+ out existing environments and scaffold necessary files.
101
+ """
102
+ project_cmds.env_add(env_name=env_name, force=force)
103
+
104
+ @env_app.command("del")
105
+ def env_del(
106
+ env_name: str = typer.Argument(..., help="Environment name to delete")
107
+ ):
108
+ """
109
+ [bold red]Delete an environment[/bold red] from your project.
110
+
111
+ Requires confirmation before removal.
112
+ """
113
+ project_cmds.env_delete(env_name=env_name)
114
+
115
+ @env_app.command("init-vscode")
116
+ def env_init_vscode(
117
+ env_name: str = typer.Argument(..., help="Environment name to initialize VS Code for"),
118
+ force: bool = typer.Option(False, "--force", help="Force overwrite existing launch.json if it's malformed or already has the configuration")
119
+ ):
120
+ """
121
+ [bold blue]Initialize VS Code launch configurations[/bold blue] for an environment.
122
+
123
+ This command will add 'Launch <env> API' and 'Launch <env> Worker' to your .vscode/launch.json,
124
+ allowing you to easily debug your environment's API and Worker using VS Code.
125
+ """
126
+ project_cmds.init_vscode_launch(env_name=env_name, force=force)
127
+
128
+
129
+ @app.command()
130
+ def run(
131
+ ctx: typer.Context,
132
+ env: str = typer.Argument("local", help="Environment to run (default: local)"),
133
+ build: bool = typer.Option(True, "--build/--no-build", help="Build images before starting"),
134
+ detach: bool = typer.Option(False, "--detach", "-d", help="Run in background"),
135
+ logs: bool = typer.Option(False, "--logs", help="Follow logs (useful with --detach)")
136
+ ):
137
+ """
138
+ [bold magenta]Run[/bold magenta] the project locally using Docker Compose.
139
+
140
+ Defaults to the 'local' environment.
141
+
142
+ [yellow]Note:[/yellow] To run a specific environment, use: [bold]flowstash run <env_name>[/bold]
143
+ """
144
+ from .core.config import load_project_config
145
+ from rich.prompt import Confirm
146
+
147
+ if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
148
+ console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
149
+
150
+ project_config = load_project_config()
151
+ if project_config:
152
+ env_mode = next((e for e in project_config.environments if e.name == env), None)
153
+ if not env_mode:
154
+ if env == "local":
155
+ if Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
156
+ project_cmds.add_environment(project_config, env_name=env)
157
+ else:
158
+ console.print(f"[red]Aborting. Run 'flowstash env add' to create it manually.[/red]")
159
+ raise typer.Exit(code=1)
160
+ else:
161
+ console.print(f"[red]Environment '{env}' not found.[/red]")
162
+ raise typer.Exit(code=1)
163
+
164
+ run_cmds.run(env=env, build=build, detach=detach, logs=logs)
165
+
166
+ @app.command()
167
+ def build(
168
+ ctx: typer.Context,
169
+ env: str = typer.Argument("local", help="Environment to build (default: local)"),
170
+ tag: str = typer.Option("latest", "--tag", "-t", help="Tag for the image")
171
+ ):
172
+ """
173
+ [bold yellow]Build[/bold yellow] project artifacts/images.
174
+
175
+ Defaults to the 'local' environment.
176
+
177
+ [yellow]Note:[/yellow] To build for a specific environment, use: [bold]flowstash build <env_name>[/bold]
178
+ """
179
+ if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
180
+ console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
181
+
182
+ from .core.config import load_project_config
183
+ from rich.prompt import Confirm
184
+
185
+ project_config = load_project_config()
186
+ if project_config:
187
+ env_mode = next((e for e in project_config.environments if e.name == env), None)
188
+ if not env_mode:
189
+ if env == "local":
190
+ if Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
191
+ project_cmds.add_environment(project_config, env_name=env)
192
+ else:
193
+ console.print(f"[red]Aborting. Run 'flowstash env add' to create it manually.[/red]")
194
+ raise typer.Exit(code=1)
195
+ else:
196
+ console.print(f"[red]Environment '{env}' not found.[/red]")
197
+ raise typer.Exit(code=1)
198
+
199
+ build_cmds.build(env=env, tag=tag)
200
+
201
+ @app.command()
202
+ def deploy(
203
+ ctx: typer.Context,
204
+ env: str = typer.Argument("prod", help="Environment to deploy to (default: prod)"),
205
+ artifact: Optional[str] = typer.Option(None, "--artifact", "-a", help="Artifact ID to deploy"),
206
+ non_interactive: bool = typer.Option(False, "--non-interactive", help="Do not ask for confirmation")
207
+ ):
208
+ """
209
+ [bold cyan]Deploy[/bold cyan] your project to the flowstash Managed Platform.
210
+
211
+ Defaults to the 'prod' environment. If 'prod' is missing, it will prompt you to set it up.
212
+
213
+ [yellow]Note:[/yellow] To deploy to a specific environment, use: [bold]flowstash deploy <env_name>[/bold]
214
+ """
215
+ from .core.config import load_project_config
216
+ from .commands.project import add_environment
217
+ from rich.prompt import Confirm
218
+
219
+ if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
220
+ console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
221
+
222
+ project_config = load_project_config()
223
+ if not project_config:
224
+ console.print("[red]No .flowstash found. Please run 'flowstash init' first.[/red]")
225
+ raise typer.Exit(code=1)
226
+
227
+ # Find the requested environment
228
+ env_mode = next((e for e in project_config.environments if e.name == env), None)
229
+
230
+ if not env_mode:
231
+ if env == "prod":
232
+ if not non_interactive and Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
233
+ # We need to pass the actual project_config object to add_environment
234
+ # But project_cmds is a module, we should be careful about cyclic imports or just use it.
235
+ project_cmds.add_environment(project_config, env_name=env)
236
+ # Reload or refresh check
237
+ project_config = load_project_config()
238
+ env_mode = next((e for e in project_config.environments if e.name == env), None)
239
+ if not env_mode:
240
+ console.print(f"[red]Environment '{env}' was not created. Aborting.[/red]")
241
+ raise typer.Exit(code=1)
242
+ else:
243
+ console.print(f"[red]Aborting. Use 'flowstash env add' to create environments manually.[/red]")
244
+ raise typer.Exit(code=1)
245
+ else:
246
+ console.print(f"[red]Environment '{env}' not found.[/red]")
247
+ console.print(f"[yellow]Available environments: {', '.join(e.name for e in project_config.environments)}[/yellow]")
248
+ console.print(f"To deploy to a specific environment, use: [bold]flowstash deploy <env_name>[/bold]")
249
+ raise typer.Exit(code=1)
250
+
251
+ deploy_cmds.deploy(env=env, artifact=artifact, non_interactive=non_interactive)
252
+
253
+ @app.command()
254
+ def whoami():
255
+ """Show current login status."""
256
+ auth_cmds.whoami()
257
+
258
+ @app.command()
259
+ def logout():
260
+ """Log out from the platform."""
261
+ auth_cmds.logout()
262
+
263
+ @app.command()
264
+ def version():
265
+ """Show the flowstash CLI version."""
266
+ console.print(f"flowstash CLI version: [bold green]{__version__}[/bold green]")
267
+
268
+ if __name__ == "__main__":
269
+ app()