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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: benchops
3
- Version: 0.2.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "benchops"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "A CLI tool to synchronize local Frappe development environments with remote servers."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14.6"
@@ -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("Post Commands")
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
- cmds_display,
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
- post_commands: list[str] | None = None,
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
- if post_commands is not None:
53
- array = tomlkit.array()
54
- for cmd in post_commands:
55
- array.append(cmd)
56
- entry["post_commands"] = array
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 builds...[/yellow]")
71
- local_runner.run(["bench", "build", "--app", self.app_name], cwd=str(app_dir.parent))
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 operations...[/yellow]")
88
- post_commands = server_config.get("post_commands", [])
89
- for cmd in post_commands:
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