benchops 0.2.0__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.
- {benchops-0.2.0 → benchops-0.3.0}/PKG-INFO +1 -1
- {benchops-0.2.0 → benchops-0.3.0}/pyproject.toml +1 -1
- {benchops-0.2.0 → benchops-0.3.0}/src/benchops/cli.py +15 -9
- {benchops-0.2.0 → benchops-0.3.0}/src/benchops/config.py +13 -6
- {benchops-0.2.0 → benchops-0.3.0}/src/benchops/deploy.py +29 -6
- {benchops-0.2.0 → benchops-0.3.0}/.gitignore +0 -0
- {benchops-0.2.0 → benchops-0.3.0}/.python-version +0 -0
- {benchops-0.2.0 → benchops-0.3.0}/README.md +0 -0
- {benchops-0.2.0 → benchops-0.3.0}/src/benchops/__init__.py +0 -0
- {benchops-0.2.0 → benchops-0.3.0}/src/benchops/auth.py +0 -0
- {benchops-0.2.0 → benchops-0.3.0}/src/benchops/runner.py +0 -0
- {benchops-0.2.0 → benchops-0.3.0}/src/benchops/sync.py +0 -0
- {benchops-0.2.0 → benchops-0.3.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: benchops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A CLI tool to synchronize local Frappe development environments with remote servers.
|
|
5
5
|
Author-email: Mohammad Hamdan <mohammadasdev@gmail.com>
|
|
6
6
|
Keywords: cli,deployment,erpnext,frappe,sync
|
|
@@ -19,6 +19,13 @@ app.add_typer(server_app, name="server")
|
|
|
19
19
|
console = Console()
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _render_hook_count(config: dict, key: str) -> str:
|
|
23
|
+
commands = config.get(key) or []
|
|
24
|
+
if commands:
|
|
25
|
+
return f"[green]{len(commands)} cmds[/green]"
|
|
26
|
+
return "[dim]None[/dim]"
|
|
27
|
+
|
|
28
|
+
|
|
22
29
|
@app.command()
|
|
23
30
|
def init() -> None:
|
|
24
31
|
"""Initialize the benchops configuration."""
|
|
@@ -53,22 +60,20 @@ def list_servers() -> None:
|
|
|
53
60
|
table.add_column("Port")
|
|
54
61
|
table.add_column("User")
|
|
55
62
|
table.add_column("Bench Path")
|
|
56
|
-
table.add_column("
|
|
63
|
+
table.add_column("Pre-Local", justify="center")
|
|
64
|
+
table.add_column("Pre-Remote", justify="center")
|
|
65
|
+
table.add_column("Post-Remote", justify="center")
|
|
57
66
|
|
|
58
67
|
for alias, config in sorted(servers.items()):
|
|
59
|
-
post_cmds = config.get("post_commands")
|
|
60
|
-
if isinstance(post_cmds, list) and post_cmds:
|
|
61
|
-
cmds_display = "\n".join(post_cmds)
|
|
62
|
-
else:
|
|
63
|
-
cmds_display = "[dim]None[/dim]"
|
|
64
|
-
|
|
65
68
|
table.add_row(
|
|
66
69
|
alias,
|
|
67
70
|
config.get("host", ""),
|
|
68
71
|
str(config.get("port", "")),
|
|
69
72
|
config.get("user", ""),
|
|
70
73
|
config.get("bench_path", ""),
|
|
71
|
-
|
|
74
|
+
_render_hook_count(config, "pre_local_commands"),
|
|
75
|
+
_render_hook_count(config, "pre_remote_commands"),
|
|
76
|
+
_render_hook_count(config, "post_remote_commands"),
|
|
72
77
|
)
|
|
73
78
|
console.print(table)
|
|
74
79
|
|
|
@@ -125,7 +130,8 @@ def remove_server(
|
|
|
125
130
|
def deploy(
|
|
126
131
|
app_name: str = typer.Argument(..., help="Name of the local Frappe app directory to sync."),
|
|
127
132
|
server_alias: str = typer.Argument(..., help="Alias of the target server."),
|
|
133
|
+
site: str | None = typer.Option(None, help="Specific site to target for remote commands (e.g., test-16.akwad.qa)."),
|
|
128
134
|
) -> None:
|
|
129
135
|
"""Deploy a local Frappe app to a remote server."""
|
|
130
|
-
command = DeployCommand(app_name, server_alias)
|
|
136
|
+
command = DeployCommand(app_name, server_alias, site)
|
|
131
137
|
command.execute()
|
|
@@ -35,7 +35,9 @@ class ConfigManager:
|
|
|
35
35
|
user: str,
|
|
36
36
|
bench_path: str,
|
|
37
37
|
private_key_path: str | None = None,
|
|
38
|
-
|
|
38
|
+
pre_local_commands: list[str] | None = None,
|
|
39
|
+
pre_remote_commands: list[str] | None = None,
|
|
40
|
+
post_remote_commands: list[str] | None = None,
|
|
39
41
|
) -> None:
|
|
40
42
|
"""Add or update a server, preserving existing comments and formatting."""
|
|
41
43
|
self.init_config()
|
|
@@ -49,11 +51,16 @@ class ConfigManager:
|
|
|
49
51
|
entry["bench_path"] = bench_path
|
|
50
52
|
if private_key_path is not None:
|
|
51
53
|
entry["private_key_path"] = private_key_path
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
for hook, commands in (
|
|
55
|
+
("pre_local_commands", pre_local_commands),
|
|
56
|
+
("pre_remote_commands", pre_remote_commands),
|
|
57
|
+
("post_remote_commands", post_remote_commands),
|
|
58
|
+
):
|
|
59
|
+
if commands is not None:
|
|
60
|
+
array = tomlkit.array()
|
|
61
|
+
for cmd in commands:
|
|
62
|
+
array.append(cmd)
|
|
63
|
+
entry[hook] = array
|
|
57
64
|
|
|
58
65
|
doc["servers"][alias] = entry
|
|
59
66
|
TOMLFile(self.config_path).write(doc)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Deployment command logic."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import shlex
|
|
4
5
|
import subprocess
|
|
5
6
|
import tempfile
|
|
6
7
|
from pathlib import Path
|
|
@@ -19,9 +20,10 @@ console = Console()
|
|
|
19
20
|
class DeployCommand:
|
|
20
21
|
"""Encapsulates the deployment pipeline logic."""
|
|
21
22
|
|
|
22
|
-
def __init__(self, app_name: str, server_alias: str) -> None:
|
|
23
|
+
def __init__(self, app_name: str, server_alias: str, site: str | None = None) -> None:
|
|
23
24
|
self.app_name = app_name
|
|
24
25
|
self.server_alias = server_alias
|
|
26
|
+
self.site = site
|
|
25
27
|
self.config = ConfigManager()
|
|
26
28
|
self.auth = AuthManager()
|
|
27
29
|
|
|
@@ -35,6 +37,17 @@ class DeployCommand:
|
|
|
35
37
|
f"Local app directory '{self.app_name}' not found (looked in '{cwd / 'apps'}' and '{cwd}')."
|
|
36
38
|
)
|
|
37
39
|
|
|
40
|
+
def _interpolate_cmd(self, cmd: str) -> str:
|
|
41
|
+
"""Replace the {site} placeholder in a command with the target site."""
|
|
42
|
+
if "{site}" not in cmd:
|
|
43
|
+
return cmd
|
|
44
|
+
if self.site is None:
|
|
45
|
+
console.print(
|
|
46
|
+
f"[red]Error: Command '{cmd}' requires a --site argument, but none was provided.[/red]"
|
|
47
|
+
)
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
return cmd.replace("{site}", self.site)
|
|
50
|
+
|
|
38
51
|
def execute(self) -> None:
|
|
39
52
|
"""Execute the full deployment pipeline."""
|
|
40
53
|
server_config = self.config.get_server(self.server_alias)
|
|
@@ -67,8 +80,11 @@ class DeployCommand:
|
|
|
67
80
|
|
|
68
81
|
try:
|
|
69
82
|
app_dir = self._resolve_app_dir()
|
|
70
|
-
console.print("[yellow]Starting local
|
|
71
|
-
|
|
83
|
+
console.print("[yellow]Starting local pre-deploy commands...[/yellow]")
|
|
84
|
+
pre_local_commands = server_config.get("pre_local_commands", [])
|
|
85
|
+
for cmd in pre_local_commands:
|
|
86
|
+
console.print(f"[cyan]Executing: {cmd}[/cyan]")
|
|
87
|
+
local_runner.run(shlex.split(cmd), cwd=str(app_dir.parent))
|
|
72
88
|
|
|
73
89
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
74
90
|
tarball = create_tarball(str(app_dir), os.path.join(tmp_dir, f"{self.app_name}.tar.gz"))
|
|
@@ -77,6 +93,12 @@ class DeployCommand:
|
|
|
77
93
|
console.print(
|
|
78
94
|
f"[yellow]Connecting to {server_config['user']}@{server_config['host']}:{server_config['port']}...[/yellow]"
|
|
79
95
|
)
|
|
96
|
+
pre_remote_commands = server_config.get("pre_remote_commands", [])
|
|
97
|
+
for cmd in pre_remote_commands:
|
|
98
|
+
cmd = self._interpolate_cmd(cmd)
|
|
99
|
+
console.print(f"[cyan]Executing: {cmd}[/cyan]")
|
|
100
|
+
remote_runner.run(cmd, cwd=server_config["bench_path"])
|
|
101
|
+
|
|
80
102
|
remote_dest_dir = f"{server_config['bench_path']}/apps"
|
|
81
103
|
remote_tar_path = transfer_tarball(remote_runner, tarball, remote_dest_dir)
|
|
82
104
|
console.print(f"[green]Transferred tarball to {remote_tar_path}[/green]")
|
|
@@ -84,9 +106,10 @@ class DeployCommand:
|
|
|
84
106
|
extract_and_cleanup(remote_runner, remote_tar_path, remote_dest_dir)
|
|
85
107
|
console.print("[green]Extracted on remote server.[/green]")
|
|
86
108
|
|
|
87
|
-
console.print("[yellow]Starting remote
|
|
88
|
-
|
|
89
|
-
for cmd in
|
|
109
|
+
console.print("[yellow]Starting remote post-deploy commands...[/yellow]")
|
|
110
|
+
post_remote_commands = server_config.get("post_remote_commands", [])
|
|
111
|
+
for cmd in post_remote_commands:
|
|
112
|
+
cmd = self._interpolate_cmd(cmd)
|
|
90
113
|
console.print(f"[cyan]Executing: {cmd}[/cyan]")
|
|
91
114
|
remote_runner.run(cmd, cwd=server_config["bench_path"])
|
|
92
115
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|