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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ odooflow = odooflow.cli:main
@@ -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.
@@ -0,0 +1,2 @@
1
+ odooflow
2
+ tests
tests/__init__.py ADDED
File without changes