odooflow-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.
- odooflow/__init__.py +1 -0
- odooflow/cli.py +105 -0
- odooflow/commands/__init__.py +0 -0
- odooflow/commands/clone_module.py +160 -0
- odooflow/commands/config.py +63 -0
- odooflow/commands/init_module_env.py +37 -0
- odooflow/commands/keygen.py +56 -0
- odooflow/commands/push.py +132 -0
- odooflow/commands/remote.py +133 -0
- odooflow/commands/sync_env.py +38 -0
- odooflow/config_manager.py +56 -0
- odooflow/utils/env.py +40 -0
- odooflow/utils/ssh.py +137 -0
- odooflow_cli-0.1.0.dist-info/METADATA +174 -0
- odooflow_cli-0.1.0.dist-info/RECORD +28 -0
- odooflow_cli-0.1.0.dist-info/WHEEL +5 -0
- odooflow_cli-0.1.0.dist-info/entry_points.txt +2 -0
- odooflow_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- odooflow_cli-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_commands_config.py +104 -0
- tests/test_commands_init_module_env.py +74 -0
- tests/test_commands_keygen.py +23 -0
- tests/test_commands_remote.py +32 -0
- tests/test_commands_sync_env.py +70 -0
- tests/test_config_manager.py +123 -0
- tests/test_utils_env.py +106 -0
- tests/test_utils_ssh.py +23 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from rich import print
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
from git import Repo, InvalidGitRepositoryError
|
|
8
|
+
|
|
9
|
+
from odooflow import config_manager
|
|
10
|
+
from odooflow.utils.env import write_env_file, read_env_file
|
|
11
|
+
|
|
12
|
+
config = config_manager.load_config()
|
|
13
|
+
ENV_FILENAME = config["env_file"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ALLOWED_SERVER_KEYS = {
|
|
17
|
+
"host",
|
|
18
|
+
"port",
|
|
19
|
+
"user",
|
|
20
|
+
"password", # For SSH connection
|
|
21
|
+
"key_path", # For SSH connection
|
|
22
|
+
"directory"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def parse_kv_pairs(pairs: List[str]) -> dict:
|
|
26
|
+
parsed = {}
|
|
27
|
+
for pair in pairs:
|
|
28
|
+
if "=" not in pair:
|
|
29
|
+
print(f"[red]Invalid format: {pair}. Use key=value.[/red]")
|
|
30
|
+
continue
|
|
31
|
+
key, value = pair.split("=", 1)
|
|
32
|
+
key = key.strip()
|
|
33
|
+
value = value.strip()
|
|
34
|
+
|
|
35
|
+
if key not in ALLOWED_SERVER_KEYS:
|
|
36
|
+
print(f"[yellow]⚠️ Ignoring unknown key: '{key}'. Allowed keys are: {', '.join(ALLOWED_SERVER_KEYS)}[/yellow]")
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
parsed[key] = value
|
|
40
|
+
return parsed
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_current_branch() -> Optional[str]:
|
|
44
|
+
try:
|
|
45
|
+
repo = Repo(".")
|
|
46
|
+
if repo.head.is_detached:
|
|
47
|
+
print("[yellow]Warning: HEAD is in a detached state.[/yellow]")
|
|
48
|
+
return None
|
|
49
|
+
return repo.active_branch.name
|
|
50
|
+
except InvalidGitRepositoryError:
|
|
51
|
+
print("[red]Error: Not a Git repository.[/red]")
|
|
52
|
+
return None
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"[red]Error while determining Git branch:[/red] {e}")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def remote(
|
|
59
|
+
add_repo: Optional[str] = typer.Option(None),
|
|
60
|
+
branch: Optional[str] = typer.Option(None),
|
|
61
|
+
server_json: Optional[str] = typer.Option(None)
|
|
62
|
+
):
|
|
63
|
+
updated = False
|
|
64
|
+
env_path = Path.cwd() / ENV_FILENAME
|
|
65
|
+
|
|
66
|
+
if not env_path.exists():
|
|
67
|
+
typer.secho(f"❌ environment file ({ENV_FILENAME}) is not found in the current directory.", fg="red")
|
|
68
|
+
raise typer.Exit(code=1)
|
|
69
|
+
|
|
70
|
+
env = read_env_file(env_path)
|
|
71
|
+
|
|
72
|
+
# Handle Git remote
|
|
73
|
+
if add_repo:
|
|
74
|
+
current_branch = branch or get_current_branch()
|
|
75
|
+
if not current_branch:
|
|
76
|
+
typer.secho("❌ Failed to detect branch. Please specify it with --branch.", fg="red")
|
|
77
|
+
raise typer.Exit()
|
|
78
|
+
|
|
79
|
+
env.setdefault("remotes", {})
|
|
80
|
+
|
|
81
|
+
if "repo" in env["remotes"]:
|
|
82
|
+
typer.secho("⚠️ Remote repo already configured.", fg="yellow")
|
|
83
|
+
if not typer.confirm("Do you want to overwrite the existing Git remote info?"):
|
|
84
|
+
typer.secho("❌ Skipped updating Git remote.", fg="red")
|
|
85
|
+
else:
|
|
86
|
+
env["remotes"]["repo"] = {
|
|
87
|
+
"url": add_repo,
|
|
88
|
+
"branch": current_branch
|
|
89
|
+
}
|
|
90
|
+
updated = True
|
|
91
|
+
else:
|
|
92
|
+
env["remotes"]["repo"] = {
|
|
93
|
+
"url": add_repo,
|
|
94
|
+
"branch": current_branch
|
|
95
|
+
}
|
|
96
|
+
updated = True
|
|
97
|
+
typer.secho(f"🔗 Git remote set to {add_repo} (branch: {current_branch})", fg="green")
|
|
98
|
+
|
|
99
|
+
# Handle server connection via JSON
|
|
100
|
+
if server_json:
|
|
101
|
+
try:
|
|
102
|
+
server_config = json.loads(server_json)
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
typer.secho("❌ Invalid JSON format for server config.", fg="red")
|
|
105
|
+
raise typer.Exit(code=1)
|
|
106
|
+
|
|
107
|
+
# Validate allowed keys
|
|
108
|
+
invalid_keys = [key for key in server_config if key not in ALLOWED_SERVER_KEYS]
|
|
109
|
+
if invalid_keys:
|
|
110
|
+
typer.secho(f"⚠️ Ignoring unknown keys: {', '.join(invalid_keys)}", fg="yellow")
|
|
111
|
+
for key in invalid_keys:
|
|
112
|
+
server_config.pop(key)
|
|
113
|
+
|
|
114
|
+
env.setdefault("remotes", {})
|
|
115
|
+
|
|
116
|
+
if "server" in env["remotes"]:
|
|
117
|
+
typer.secho("⚠️ Remote server already configured.", fg="yellow")
|
|
118
|
+
if not typer.confirm("Do you want to overwrite the existing server connection info?"):
|
|
119
|
+
typer.secho("❌ Skipped updating server connection.", fg="red")
|
|
120
|
+
else:
|
|
121
|
+
env["remotes"]["server"] = server_config
|
|
122
|
+
updated = True
|
|
123
|
+
else:
|
|
124
|
+
env["remotes"]["server"] = server_config
|
|
125
|
+
updated = True
|
|
126
|
+
|
|
127
|
+
typer.secho("🌐 Server connection info saved.", fg="green")
|
|
128
|
+
|
|
129
|
+
if updated:
|
|
130
|
+
write_env_file(env_path, env)
|
|
131
|
+
typer.secho("💾 Remote configuration updated successfully.", fg="green")
|
|
132
|
+
elif not (add_repo or server_json):
|
|
133
|
+
typer.secho("ℹ️ No options provided. Use --help to see available flags.", fg="yellow")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from odooflow import config_manager
|
|
7
|
+
from odooflow.utils.env import read_manifest, write_env_file, read_env_file
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def sync_env(keys: Optional[str] = typer.Option(None)):
|
|
11
|
+
"""
|
|
12
|
+
Sync selected keys from __manifest__.py into the .env file.
|
|
13
|
+
Uses default keys from .odooflowrc unless overridden by --keys option.
|
|
14
|
+
"""
|
|
15
|
+
cwd = Path.cwd()
|
|
16
|
+
config = config_manager.load_config()
|
|
17
|
+
manifest_path = cwd / config.get("manifest_file", "__manifest__.py")
|
|
18
|
+
env_path = cwd / config.get("env_file", ".odooflow.env.json")
|
|
19
|
+
keys_to_sync = [k.strip() for k in keys.split(",") if k.strip()] if keys else config.get("sync_keys", ["name", "author", "license", "version", "depends"])
|
|
20
|
+
|
|
21
|
+
if not manifest_path.exists():
|
|
22
|
+
typer.secho(f"❌ {manifest_path.name} not found in the current directory.", fg="red")
|
|
23
|
+
raise typer.Exit(code=1)
|
|
24
|
+
|
|
25
|
+
manifest = read_manifest(manifest_path)
|
|
26
|
+
env = read_env_file(env_path)
|
|
27
|
+
|
|
28
|
+
updated = {}
|
|
29
|
+
for key in keys_to_sync:
|
|
30
|
+
if key in manifest:
|
|
31
|
+
env[key] = manifest[key]
|
|
32
|
+
updated[key] = manifest[key]
|
|
33
|
+
|
|
34
|
+
if updated:
|
|
35
|
+
write_env_file(env_path, env)
|
|
36
|
+
typer.secho(f"✅ Synced keys: {', '.join(updated.keys())}", fg="cyan")
|
|
37
|
+
else:
|
|
38
|
+
typer.secho("⚠️ No matching keys found to sync.", fg="yellow")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import typer
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
DEFAULT_CONFIG = {
|
|
8
|
+
"env_file": ".odooflow.env.json",
|
|
9
|
+
"manifest_file": "__manifest__.py",
|
|
10
|
+
"core_modules" : ["base", "web", "mail", "sale", "account"],
|
|
11
|
+
"sync_keys" : ["name", "author", "license", "version", "depends"],
|
|
12
|
+
"gitlab_url": "https://gitlab.ebtech-solution.com",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
CONFIG_FILENAME = ".odooflowrc"
|
|
16
|
+
|
|
17
|
+
def get_global_config_path() -> Path:
|
|
18
|
+
return Path.home() / ".odooflowrc"
|
|
19
|
+
|
|
20
|
+
def load_config() -> Dict:
|
|
21
|
+
path = get_global_config_path()
|
|
22
|
+
if path.exists():
|
|
23
|
+
try:
|
|
24
|
+
return json.loads(path.read_text())
|
|
25
|
+
except json.JSONDecodeError as e:
|
|
26
|
+
typer.secho(
|
|
27
|
+
f"❌ Error: Config file at {path} is corrupted (invalid JSON: {e}). "
|
|
28
|
+
f"Please fix or delete the file before running again to avoid overwriting it.",
|
|
29
|
+
fg="red",
|
|
30
|
+
bold=True,
|
|
31
|
+
)
|
|
32
|
+
raise typer.Exit(code=1)
|
|
33
|
+
return DEFAULT_CONFIG.copy()
|
|
34
|
+
|
|
35
|
+
def save_config(config: Dict):
|
|
36
|
+
path = get_global_config_path()
|
|
37
|
+
with open(path, "w") as f:
|
|
38
|
+
json.dump(config, f, indent=4)
|
|
39
|
+
|
|
40
|
+
def get_access_token():
|
|
41
|
+
token = os.getenv("ODOOFLOW_ACCESS_TOKEN")
|
|
42
|
+
if token:
|
|
43
|
+
return token
|
|
44
|
+
|
|
45
|
+
config = load_config()
|
|
46
|
+
token = config.get("access_token")
|
|
47
|
+
if token:
|
|
48
|
+
return token
|
|
49
|
+
|
|
50
|
+
typer.secho("❌ Access token not found. Please set the 'ODOOFLOW_ACCESS_TOKEN' "
|
|
51
|
+
"environment variable or configure it via `.odooflowrc`.", fg="red", bold=True)
|
|
52
|
+
raise typer.Exit(code=1)
|
|
53
|
+
|
|
54
|
+
def get_core_modules_from_config() -> set:
|
|
55
|
+
config = load_config()
|
|
56
|
+
return set(config.get("core_modules", DEFAULT_CONFIG.get("core_modules", [])))
|
odooflow/utils/env.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import json
|
|
3
|
+
import typer
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from pprint import pformat
|
|
6
|
+
from rich import print
|
|
7
|
+
|
|
8
|
+
from odooflow.config_manager import load_config
|
|
9
|
+
|
|
10
|
+
config = load_config()
|
|
11
|
+
ENV_FILENAME = config["env_file"]
|
|
12
|
+
MANIFEST_FILENAME = config["manifest_file"]
|
|
13
|
+
|
|
14
|
+
def read_manifest(path: Path):
|
|
15
|
+
try:
|
|
16
|
+
return ast.literal_eval(path.read_text())
|
|
17
|
+
except (SyntaxError, ValueError) as e:
|
|
18
|
+
typer.secho(f"❌ Error parsing manifest {path}: {e}", fg="red", bold=True)
|
|
19
|
+
raise typer.Exit(1)
|
|
20
|
+
|
|
21
|
+
def update_manifest(path: Path, updates: dict):
|
|
22
|
+
manifest = read_manifest(path)
|
|
23
|
+
manifest.update(updates)
|
|
24
|
+
|
|
25
|
+
formatted_manifest = pformat(manifest, indent=4, width=100)
|
|
26
|
+
content = f"# Automatically updated by odooflow\n{formatted_manifest}"
|
|
27
|
+
|
|
28
|
+
path.write_text(content)
|
|
29
|
+
print(f"[green]Updated {MANIFEST_FILENAME}[/green]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def write_env_file(env_path: Path, values: dict):
|
|
33
|
+
env_path.write_text(json.dumps(values, indent=4))
|
|
34
|
+
print(f"[green]Created {ENV_FILENAME}[/green]")
|
|
35
|
+
|
|
36
|
+
def read_env_file(path: Path) -> dict:
|
|
37
|
+
try:
|
|
38
|
+
return json.loads(path.read_text())
|
|
39
|
+
except Exception:
|
|
40
|
+
return {}
|
odooflow/utils/ssh.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tarfile
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Set
|
|
6
|
+
import paramiko
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_remote_path(sftp, path: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Resolves remote path (supports ~, relative, absolute).
|
|
13
|
+
"""
|
|
14
|
+
if path.startswith("~"):
|
|
15
|
+
home = sftp.normalize(".")
|
|
16
|
+
return path.replace("~", home, 1)
|
|
17
|
+
elif not path.startswith("/"):
|
|
18
|
+
home = sftp.normalize(".")
|
|
19
|
+
return f"{home}/{path}"
|
|
20
|
+
else:
|
|
21
|
+
return path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compress_directory(source_dir: Path, exclude_dirs: Optional[Set[str]] = None) -> Path:
|
|
25
|
+
"""
|
|
26
|
+
Compress a directory into a .tar.gz archive in a cross-platform safe temp location.
|
|
27
|
+
"""
|
|
28
|
+
exclude_dirs = exclude_dirs or set()
|
|
29
|
+
print(f"🔧 Compressing directory: {source_dir}")
|
|
30
|
+
archive_fd, archive_path = tempfile.mkstemp(suffix=".tar.gz")
|
|
31
|
+
os.close(archive_fd)
|
|
32
|
+
|
|
33
|
+
with tarfile.open(archive_path, "w:gz") as tar:
|
|
34
|
+
for root, dirs, files in os.walk(source_dir):
|
|
35
|
+
rel_root = Path(root).relative_to(source_dir)
|
|
36
|
+
if any(part in exclude_dirs for part in rel_root.parts):
|
|
37
|
+
continue
|
|
38
|
+
for file in files:
|
|
39
|
+
file_path = Path(root) / file
|
|
40
|
+
arcname = Path(source_dir.name) / rel_root / file
|
|
41
|
+
tar.add(file_path, arcname=str(arcname).replace("\\", "/"))
|
|
42
|
+
|
|
43
|
+
print(f"✅ Compression complete: {archive_path}")
|
|
44
|
+
return Path(archive_path)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def upload_directory_via_ssh(
|
|
48
|
+
local_path: Path,
|
|
49
|
+
remote_user: str,
|
|
50
|
+
remote_host: str,
|
|
51
|
+
remote_path: str,
|
|
52
|
+
port: int = 22,
|
|
53
|
+
key_path: Optional[str] = None,
|
|
54
|
+
password: Optional[str] = None,
|
|
55
|
+
exclude_dirs: Optional[Set[str]] = None,
|
|
56
|
+
strict_host_key_checking: bool = False,
|
|
57
|
+
post_exec_cmd: Optional[str] = None,
|
|
58
|
+
on_post_exec=None,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Uploads a local directory to a remote server via SSH by compressing it and extracting it remotely.
|
|
62
|
+
Cross-platform compatible.
|
|
63
|
+
|
|
64
|
+
If `post_exec_cmd` is provided, it is executed over the same SSH connection after a successful
|
|
65
|
+
extract/cleanup (and before the connection is closed). `on_post_exec(stdout, stderr, exit_status)`
|
|
66
|
+
is an optional callback that receives the command's streams and exit status for custom reporting.
|
|
67
|
+
"""
|
|
68
|
+
exclude_dirs = exclude_dirs or set()
|
|
69
|
+
local_path = Path(local_path).resolve()
|
|
70
|
+
archive_name = f"{local_path.name}.tar.gz"
|
|
71
|
+
|
|
72
|
+
print(f"🔐 Connecting to {remote_user}@{remote_host}:{port} ...")
|
|
73
|
+
ssh = paramiko.SSHClient()
|
|
74
|
+
|
|
75
|
+
if strict_host_key_checking:
|
|
76
|
+
ssh.load_system_host_keys()
|
|
77
|
+
ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
|
|
78
|
+
else:
|
|
79
|
+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
80
|
+
|
|
81
|
+
if key_path:
|
|
82
|
+
key_path = os.path.expanduser(key_path)
|
|
83
|
+
pkey = paramiko.RSAKey.from_private_key_file(key_path)
|
|
84
|
+
ssh.connect(remote_host, port=port, username=remote_user, pkey=pkey)
|
|
85
|
+
else:
|
|
86
|
+
ssh.connect(remote_host, port=port, username=remote_user, password=password)
|
|
87
|
+
|
|
88
|
+
sftp = ssh.open_sftp()
|
|
89
|
+
resolved_remote_path = resolve_remote_path(sftp, remote_path)
|
|
90
|
+
print(f"📁 Remote path resolved to: {resolved_remote_path}")
|
|
91
|
+
|
|
92
|
+
# Step 1: Compress local directory
|
|
93
|
+
archive_path = compress_directory(local_path, exclude_dirs)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Step 2: Upload archive
|
|
97
|
+
remote_archive = f"{resolved_remote_path}/{archive_name}"
|
|
98
|
+
print(f"📤 Uploading archive to remote: {remote_archive}")
|
|
99
|
+
sftp.put(str(archive_path), remote_archive)
|
|
100
|
+
print(f"✅ Upload complete.")
|
|
101
|
+
|
|
102
|
+
# Step 3: Extract archive on remote server
|
|
103
|
+
print(f"📦 Extracting archive on remote server ...")
|
|
104
|
+
extract_cmd = f"mkdir -p {resolved_remote_path} && tar -xzf {remote_archive} -C {resolved_remote_path}"
|
|
105
|
+
stdin, stdout, stderr = ssh.exec_command(extract_cmd)
|
|
106
|
+
if stdout.channel.recv_exit_status() != 0:
|
|
107
|
+
raise RuntimeError(stderr.read().decode())
|
|
108
|
+
print(f"✅ Extraction complete.")
|
|
109
|
+
|
|
110
|
+
# Step 4: Remove remote archive
|
|
111
|
+
print(f"🧹 Cleaning up remote archive ...")
|
|
112
|
+
ssh.exec_command(f"rm -f {remote_archive}")
|
|
113
|
+
print(f"✅ Remote cleanup complete.")
|
|
114
|
+
|
|
115
|
+
# Step 5: Optionally run a post-upload command on the remote host
|
|
116
|
+
if post_exec_cmd:
|
|
117
|
+
print(f"⚙️ Running post-upload command: {post_exec_cmd}")
|
|
118
|
+
stdin, stdout, stderr = ssh.exec_command(post_exec_cmd)
|
|
119
|
+
exit_status = stdout.channel.recv_exit_status()
|
|
120
|
+
out_text = stdout.read().decode()
|
|
121
|
+
err_text = stderr.read().decode()
|
|
122
|
+
if on_post_exec:
|
|
123
|
+
on_post_exec(out_text, err_text, exit_status)
|
|
124
|
+
elif exit_status != 0:
|
|
125
|
+
raise RuntimeError(err_text or f"Post-upload command failed with exit status {exit_status}")
|
|
126
|
+
print(f"✅ Post-upload command complete.")
|
|
127
|
+
|
|
128
|
+
finally:
|
|
129
|
+
# Step 6: Clean up local archive
|
|
130
|
+
print(f"🧼 Removing temporary local archive ...")
|
|
131
|
+
if archive_path.exists():
|
|
132
|
+
archive_path.unlink()
|
|
133
|
+
print(f"✅ Local cleanup complete.")
|
|
134
|
+
|
|
135
|
+
sftp.close()
|
|
136
|
+
ssh.close()
|
|
137
|
+
print(f"🔒 SSH connection closed.")
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odooflow-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OdooFlow CLI - streamline your Odoo development workflow
|
|
5
|
+
Author: Mohammad A. Hamdan
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/anomalyco/odooflow
|
|
8
|
+
Project-URL: Repository, https://github.com/anomalyco/odooflow
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: typer[all]
|
|
19
|
+
Requires-Dist: rich
|
|
20
|
+
Requires-Dist: GitPython>=3.1.44
|
|
21
|
+
Requires-Dist: requests>=2.32.3
|
|
22
|
+
Requires-Dist: paramiko>=3.5.1
|
|
23
|
+
Requires-Dist: tqdm>=4.67.1
|
|
24
|
+
Requires-Dist: bcrypt>=4.3.0
|
|
25
|
+
Requires-Dist: cryptography>=45.0.3
|
|
26
|
+
Requires-Dist: PyNaCl>=1.5.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=6.1.0; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# 🌀 Odooflow CLI
|
|
35
|
+
|
|
36
|
+
**Odooflow CLI** is a command-line interface tool designed to streamline the development workflow for Odoo projects. It helps clone Odoo modules (and their dependencies), handles GitLab lookups, and provides options for deep recursive cloning.
|
|
37
|
+
|
|
38
|
+
## 🚀 Features
|
|
39
|
+
|
|
40
|
+
- Clone an Odoo module by Git URL
|
|
41
|
+
- Recursively resolve and clone all dependencies
|
|
42
|
+
- Smart skip of Odoo core modules
|
|
43
|
+
- Branch selection for cloning
|
|
44
|
+
- Helpful and colorful CLI output
|
|
45
|
+
- Built using [Typer](https://typer.tiangolo.com/) and Python 3.9+
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 📦 Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/YOUR_USERNAME/odooflow-cli.git
|
|
53
|
+
cd odooflow-cli
|
|
54
|
+
pip install .
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or install directly from source for development:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install -e .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 🛠️ Usage
|
|
66
|
+
|
|
67
|
+
Once installed, you can use the CLI by running:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
odooflow --help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Available Commands:
|
|
74
|
+
|
|
75
|
+
- **`init`**: Initialize the Odoo module environment file and sync metadata with manifest
|
|
76
|
+
- **`sync-env`**: Sync the environment file from manifest
|
|
77
|
+
- **`config`**: Update or show OdooFlow CLI configuration
|
|
78
|
+
- **`clone`**: Clone a module and its dependencies from a git repository
|
|
79
|
+
- **`remote`**: Manage remote connections for Git and deployment server
|
|
80
|
+
- **`ssh-keygen`**: Generate a secure SSH key pair
|
|
81
|
+
- **`push`**: Push the current Git branch and upload the project to the test server
|
|
82
|
+
|
|
83
|
+
### Clone Command Options:
|
|
84
|
+
|
|
85
|
+
| Flag | Description |
|
|
86
|
+
|-------------|------------------------------------------|
|
|
87
|
+
| `--url` | Full HTTP URL of the module repo |
|
|
88
|
+
| `--branch` | (Optional) Git branch to clone from |
|
|
89
|
+
| `--deep` | Recursively clone all dependencies |
|
|
90
|
+
|
|
91
|
+
### 🔍 Examples:
|
|
92
|
+
|
|
93
|
+
Clone a single module:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
odooflow clone --url https://gitlab.com/mygroup/my_odoo_module.git
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Clone with specific branch:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
odooflow clone --url https://gitlab.com/mygroup/my_odoo_module.git --branch 17.0
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Clone deeply with dependencies:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
odooflow clone --url https://gitlab.com/mygroup/my_odoo_module.git --deep
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 📁 Project Structure
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
odooflow/
|
|
117
|
+
├── odooflow/
|
|
118
|
+
│ ├── __init__.py
|
|
119
|
+
│ ├── cli.py
|
|
120
|
+
│ ├── config_manager.py
|
|
121
|
+
│ ├── commands/
|
|
122
|
+
│ │ ├── __init__.py
|
|
123
|
+
│ │ ├── clone_module.py
|
|
124
|
+
│ │ ├── config.py
|
|
125
|
+
│ │ ├── init_module_env.py
|
|
126
|
+
│ │ ├── keygen.py
|
|
127
|
+
│ │ ├── push.py
|
|
128
|
+
│ │ ├── remote.py
|
|
129
|
+
│ │ └── sync_env.py
|
|
130
|
+
│ └── utils/
|
|
131
|
+
│ ├── env.py
|
|
132
|
+
│ └── ssh.py
|
|
133
|
+
├── tests/
|
|
134
|
+
├── README.md
|
|
135
|
+
├── requirements.txt
|
|
136
|
+
├── setup.py
|
|
137
|
+
└── LICENSE
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 🤝 Contributing
|
|
143
|
+
|
|
144
|
+
Contributions are welcome! Please open an issue or submit a pull request with any improvements, bug fixes, or new features.
|
|
145
|
+
|
|
146
|
+
1. Fork the repository
|
|
147
|
+
2. Create a new branch (`git checkout -b feature/your-feature`)
|
|
148
|
+
3. Commit your changes (`git commit -am 'Add new feature'`)
|
|
149
|
+
4. Push to the branch (`git push origin feature/your-feature`)
|
|
150
|
+
5. Open a Pull Request
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 📝 License
|
|
155
|
+
|
|
156
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## WISH-LIST:
|
|
161
|
+
|
|
162
|
+
Move this CLI into fully-integrated Odoo environment, using Odoo, users can create issues, add the amount of details, then sync these issues with Odooflow.
|
|
163
|
+
|
|
164
|
+
We can do integration with any code agent to help developers to achieve these issues
|
|
165
|
+
|
|
166
|
+
same thing for pipelines, I think it will be amazing if developers can build pipelines using Odoo, then apply the same pipelines using Odooflow.
|
|
167
|
+
|
|
168
|
+
I have many things in my head, I will back soon to this project.
|
|
169
|
+
|
|
170
|
+
## 👨💻 Author
|
|
171
|
+
|
|
172
|
+
Made with ❤️ by Mohammad A. Hamdan
|
|
173
|
+
|
|
174
|
+
---
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
odooflow/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
odooflow/cli.py,sha256=QysSCz88yhntP4DFfrGbNjdJ4GNKVz5sG59PMB3uBiU,4240
|
|
3
|
+
odooflow/config_manager.py,sha256=xOG0Kr6E8EjS6LZ1hcyfHdQbaoPvGGZMrFKZDaWDSNY,1734
|
|
4
|
+
odooflow/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
odooflow/commands/clone_module.py,sha256=lIeCtEfHjT7NPz4IbX77iBi4BAg4vHDzBGkk3v-4qDs,5893
|
|
6
|
+
odooflow/commands/config.py,sha256=Trups61UB3rP7drZQ3UfPQD986ELKMe7FOpi4cn8qb0,2238
|
|
7
|
+
odooflow/commands/init_module_env.py,sha256=r-9sioBEBI4iRzyNvwqihTWxPhhQ-LevVd5EdcRlLq0,1347
|
|
8
|
+
odooflow/commands/keygen.py,sha256=aOq5gxYBilB4ioU5r3J0-8RqFB-mgQZMKjUI5V4m7f4,1947
|
|
9
|
+
odooflow/commands/push.py,sha256=ppoKJyUe5LQAxuWYruz7B_sVWu-iaCN9j27GTd2iMw0,4746
|
|
10
|
+
odooflow/commands/remote.py,sha256=CcQ5EYX3vxwwjewl8MfKr-g0EOu8TyyzJADF_kdCm5M,4474
|
|
11
|
+
odooflow/commands/sync_env.py,sha256=DCMM3j6D8WZh_FEc8TfUaJCmD9oYck7ZLOXdxfv2Zyw,1371
|
|
12
|
+
odooflow/utils/env.py,sha256=VqIXYDFH3Ci1DirjSuC1mD1qwEIRc6v3T6BmHO0Xf2w,1139
|
|
13
|
+
odooflow/utils/ssh.py,sha256=KyykxXvzqiIdHFFGobIKQ8FSAnsX8HPOljtW7koVHlo,5183
|
|
14
|
+
odooflow_cli-0.1.0.dist-info/licenses/LICENSE,sha256=04Qeq0BZOsOYrA1BbTaD24l1bs8W65iJ16yDr2jUo-8,1072
|
|
15
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
tests/test_commands_config.py,sha256=ngDBMyF45VL6vPyCmC-GsIgkztZXzeodcUq6D1A_vNk,3799
|
|
17
|
+
tests/test_commands_init_module_env.py,sha256=xLWeiDiVz0LNKU-v4F6dNlJoe6FjnEln4qMi1DHje_k,2419
|
|
18
|
+
tests/test_commands_keygen.py,sha256=6WLrXRZpuECRFEXpXmipim-y8Dr0bxS0t7qYNaHIZLQ,849
|
|
19
|
+
tests/test_commands_remote.py,sha256=s-t5nZuYUQFIlInARRWhqAi8HphIErzGWD1YrRY1A3U,1441
|
|
20
|
+
tests/test_commands_sync_env.py,sha256=T8a06VoK1bi12qIf9XhkfxsVD4cGCHWqpVRnNpcXYoI,2318
|
|
21
|
+
tests/test_config_manager.py,sha256=ogBOA_FwyanfLGWQcy3kRWlyIQRtHtErysL0pOftARQ,4466
|
|
22
|
+
tests/test_utils_env.py,sha256=A5TcxQeDcQ27itB7Cyc8KfyQm62AvMAYs8FGR-SFAZ0,3635
|
|
23
|
+
tests/test_utils_ssh.py,sha256=6mzaIBtjznAByQkmNQ5-QeUo9MEq5XXqko1kVLSVHf4,837
|
|
24
|
+
odooflow_cli-0.1.0.dist-info/METADATA,sha256=HvAwdle77laJ_YF-wmZMAebHKIeOUPtjmHhmTzYQJJQ,4877
|
|
25
|
+
odooflow_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
26
|
+
odooflow_cli-0.1.0.dist-info/entry_points.txt,sha256=yVp5bn3BO-hnfyY1acflV9piaSOS78gZZTMDk7VKVhA,47
|
|
27
|
+
odooflow_cli-0.1.0.dist-info/top_level.txt,sha256=t25nuFaYQL719jBtwqSL3PQrx4vemAU927Sdl4JxBr4,15
|
|
28
|
+
odooflow_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mohammad Hamdan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tests/__init__.py
ADDED
|
File without changes
|