odoo-dev 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.
odoo_dev/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Odoo Development Environment Helper."""
2
+
3
+ __version__ = "0.1.0"
odoo_dev/cli.py ADDED
@@ -0,0 +1,36 @@
1
+ """Main CLI entry point for odoo-dev."""
2
+
3
+ import typer
4
+
5
+ from odoo_dev.commands import db, docker, run, setup
6
+
7
+ app = typer.Typer(
8
+ name="odoo-dev",
9
+ help="Odoo Development Environment Helper",
10
+ no_args_is_help=True,
11
+ )
12
+
13
+ # Command groups
14
+ app.add_typer(db.app, name="db")
15
+ app.add_typer(docker.app, name="docker")
16
+
17
+ # Setup commands
18
+ app.command()(setup.setup)
19
+ app.command(name="setup-venv")(setup.setup_venv)
20
+ app.command()(setup.vscode)
21
+
22
+ # Local runtime commands (the defaults)
23
+ app.command()(run.run)
24
+ app.command()(run.shell)
25
+ app.command()(run.update)
26
+ app.command()(run.test)
27
+ app.command()(run.scaffold)
28
+
29
+
30
+ def main() -> None:
31
+ """Entry point for the CLI."""
32
+ app()
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1 @@
1
+ """CLI commands for odoo-dev."""
@@ -0,0 +1,332 @@
1
+ """Database management commands."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from odoo_dev.config import load_config
10
+ from odoo_dev.utils.console import error, success, warning
11
+
12
+ app = typer.Typer(help="Database operations")
13
+
14
+
15
+ @app.command()
16
+ def restore(
17
+ backup_file: Annotated[
18
+ Path, typer.Argument(help="Path to backup file (.zip, .sql, .dump)")
19
+ ],
20
+ db_name: Annotated[
21
+ str | None,
22
+ typer.Argument(help="Database name (default: derived from filename)"),
23
+ ] = None,
24
+ no_neutralize: Annotated[
25
+ bool, typer.Option("--no-neutralize", help="Skip neutralization")
26
+ ] = False,
27
+ ) -> None:
28
+ """Restore database from backup file."""
29
+ cfg = load_config()
30
+
31
+ # Resolve to absolute path
32
+ backup_file = backup_file.resolve()
33
+
34
+ if not backup_file.exists():
35
+ error(f"Backup file not found: {backup_file}")
36
+ raise typer.Exit(1)
37
+
38
+ # Derive db name from filename if not provided
39
+ if db_name is None:
40
+ db_name = backup_file.stem
41
+
42
+ # Check config exists
43
+ if not cfg.config_file.exists():
44
+ error(f"Config file not found: {cfg.config_file}")
45
+ error("Run 'odoo-dev setup' first to create the configuration.")
46
+ raise typer.Exit(1)
47
+
48
+ neutralize = not no_neutralize
49
+ file_ext = backup_file.suffix.lower()
50
+
51
+ success(f"Restoring database {db_name} from {backup_file}...")
52
+ success(f"Format: {file_ext}")
53
+ success(f"Neutralize: {neutralize}")
54
+
55
+ # Parse database connection from config
56
+ db_config = _parse_db_config(cfg.config_file)
57
+
58
+ # Check if database exists
59
+ if _database_exists(db_name, db_config):
60
+ warning(f"Database {db_name} already exists.")
61
+ if not typer.confirm(
62
+ "Do you want to drop it and restore from backup?", default=False
63
+ ):
64
+ success("Restore cancelled.")
65
+ raise typer.Exit(0)
66
+
67
+ # Activate venv if needed
68
+ venv_activate = cfg.venv_path / "bin" / "activate"
69
+ if not venv_activate.exists():
70
+ error(f"Virtual environment not found at {cfg.venv_path}")
71
+ error("Run 'odoo-dev setup-venv' first.")
72
+ raise typer.Exit(1)
73
+
74
+ # Drop existing database
75
+ warning(f"Dropping existing database {db_name}...")
76
+ _run_odoo_cmd(cfg, ["db", "drop", db_name], check=False)
77
+
78
+ # Restore based on file type
79
+ neutralize_flag = ["-n"] if neutralize else []
80
+
81
+ if file_ext == ".zip":
82
+ success("Using odoo db load method...")
83
+ result = _run_odoo_cmd(
84
+ cfg, ["db", "load", *neutralize_flag, db_name, str(backup_file)]
85
+ )
86
+ elif file_ext == ".dump":
87
+ success("Using pg_restore method...")
88
+ _create_database(db_name, db_config)
89
+ _pg_restore(backup_file, db_name, db_config)
90
+ if neutralize:
91
+ _run_neutralize(cfg, db_name, db_config)
92
+ elif file_ext == ".sql":
93
+ success("Using psql method...")
94
+ _create_database(db_name, db_config)
95
+ _psql_restore(backup_file, db_name, db_config)
96
+ if neutralize:
97
+ _run_neutralize(cfg, db_name, db_config)
98
+ else:
99
+ error(f"Unsupported backup format: {file_ext}")
100
+ error("Supported formats: .zip, .sql, .dump")
101
+ raise typer.Exit(1)
102
+
103
+ success(f"Database {db_name} restored successfully!")
104
+
105
+
106
+ @app.command()
107
+ def drop(
108
+ db_name: Annotated[str, typer.Argument(help="Database name to drop")],
109
+ ) -> None:
110
+ """Drop a database."""
111
+ cfg = load_config()
112
+
113
+ warning(f"You are about to drop database {db_name}. This cannot be undone.")
114
+ if not typer.confirm("Continue?", default=False):
115
+ success("Cancelled.")
116
+ raise typer.Exit(0)
117
+
118
+ success(f"Dropping database {db_name}...")
119
+ _run_odoo_cmd(cfg, ["db", "drop", db_name])
120
+ success(f"Database {db_name} dropped.")
121
+
122
+
123
+ @app.command(name="list")
124
+ def list_dbs() -> None:
125
+ """List all databases."""
126
+ cfg = load_config()
127
+ success("Listing databases...")
128
+ _run_odoo_cmd(cfg, ["db", "list"])
129
+
130
+
131
+ @app.command()
132
+ def neutralize(
133
+ db_name: Annotated[str, typer.Argument(help="Database name to neutralize")],
134
+ ) -> None:
135
+ """Neutralize a database (disable emails, crons, etc.)."""
136
+ cfg = load_config()
137
+
138
+ success(f"Neutralizing database {db_name}...")
139
+ _run_odoo_cmd(cfg, ["neutralize", "-d", db_name])
140
+ success(f"Database {db_name} neutralized.")
141
+
142
+
143
+ def _parse_db_config(config_file: Path) -> dict[str, str]:
144
+ """Parse database connection info from odoo.conf."""
145
+ config: dict[str, str] = {
146
+ "host": "localhost",
147
+ "port": "5432",
148
+ "user": "odoo",
149
+ "password": "",
150
+ }
151
+
152
+ for line in config_file.read_text().splitlines():
153
+ line = line.strip()
154
+ if line.startswith("db_host"):
155
+ config["host"] = line.split("=", 1)[1].strip()
156
+ elif line.startswith("db_port"):
157
+ config["port"] = line.split("=", 1)[1].strip()
158
+ elif line.startswith("db_user"):
159
+ config["user"] = line.split("=", 1)[1].strip()
160
+ elif line.startswith("db_password"):
161
+ config["password"] = line.split("=", 1)[1].strip()
162
+
163
+ return config
164
+
165
+
166
+ def _database_exists(db_name: str, db_config: dict[str, str]) -> bool:
167
+ """Check if a database exists."""
168
+ env = subprocess.os.environ.copy()
169
+ env["PGPASSWORD"] = db_config["password"]
170
+
171
+ result = subprocess.run(
172
+ [
173
+ "psql",
174
+ "-h",
175
+ db_config["host"],
176
+ "-p",
177
+ db_config["port"],
178
+ "-U",
179
+ db_config["user"],
180
+ "-lqt",
181
+ ],
182
+ capture_output=True,
183
+ text=True,
184
+ env=env,
185
+ )
186
+
187
+ for line in result.stdout.splitlines():
188
+ if db_name == line.split("|")[0].strip():
189
+ return True
190
+ return False
191
+
192
+
193
+ def _create_database(db_name: str, db_config: dict[str, str]) -> None:
194
+ """Create a new database."""
195
+ env = subprocess.os.environ.copy()
196
+ env["PGPASSWORD"] = db_config["password"]
197
+
198
+ subprocess.run(
199
+ [
200
+ "createdb",
201
+ "-h",
202
+ db_config["host"],
203
+ "-p",
204
+ db_config["port"],
205
+ "-U",
206
+ db_config["user"],
207
+ db_name,
208
+ ],
209
+ env=env,
210
+ check=True,
211
+ )
212
+
213
+
214
+ def _pg_restore(backup_file: Path, db_name: str, db_config: dict[str, str]) -> None:
215
+ """Restore using pg_restore."""
216
+ env = subprocess.os.environ.copy()
217
+ env["PGPASSWORD"] = db_config["password"]
218
+
219
+ subprocess.run(
220
+ [
221
+ "pg_restore",
222
+ "-h",
223
+ db_config["host"],
224
+ "-p",
225
+ db_config["port"],
226
+ "-U",
227
+ db_config["user"],
228
+ "-d",
229
+ db_name,
230
+ "--no-owner",
231
+ str(backup_file),
232
+ ],
233
+ env=env,
234
+ )
235
+
236
+
237
+ def _psql_restore(backup_file: Path, db_name: str, db_config: dict[str, str]) -> None:
238
+ """Restore using psql."""
239
+ env = subprocess.os.environ.copy()
240
+ env["PGPASSWORD"] = db_config["password"]
241
+
242
+ subprocess.run(
243
+ [
244
+ "psql",
245
+ "-h",
246
+ db_config["host"],
247
+ "-p",
248
+ db_config["port"],
249
+ "-U",
250
+ db_config["user"],
251
+ "-d",
252
+ db_name,
253
+ "-f",
254
+ str(backup_file),
255
+ ],
256
+ env=env,
257
+ )
258
+
259
+
260
+ def _run_neutralize(
261
+ cfg: "ProjectConfig", db_name: str, db_config: dict[str, str]
262
+ ) -> None:
263
+ """Run Odoo neutralization and re-init db params."""
264
+ from odoo_dev.config import ProjectConfig
265
+
266
+ # Re-init database parameters
267
+ _reinit_db_params(db_name, db_config)
268
+
269
+ # Run odoo neutralize
270
+ _run_odoo_cmd(cfg, ["neutralize", "-d", db_name])
271
+
272
+
273
+ def _reinit_db_params(db_name: str, db_config: dict[str, str]) -> None:
274
+ """Re-initialize database parameters after restore."""
275
+ env = subprocess.os.environ.copy()
276
+ env["PGPASSWORD"] = db_config["password"]
277
+
278
+ sql = """
279
+ DO $$
280
+ DECLARE
281
+ new_secret TEXT := gen_random_uuid()::text;
282
+ new_uuid TEXT := gen_random_uuid()::text;
283
+ BEGIN
284
+ DELETE FROM ir_config_parameter WHERE key IN (
285
+ 'database.secret',
286
+ 'database.uuid',
287
+ 'database.create_date',
288
+ 'web.base.url',
289
+ 'base.login_cooldown_after',
290
+ 'base.login_cooldown_duration'
291
+ );
292
+
293
+ INSERT INTO ir_config_parameter (key, value, create_uid, create_date, write_uid, write_date) VALUES
294
+ ('database.secret', new_secret, 1, LOCALTIMESTAMP, 1, LOCALTIMESTAMP),
295
+ ('database.uuid', new_uuid, 1, LOCALTIMESTAMP, 1, LOCALTIMESTAMP),
296
+ ('database.create_date', LOCALTIMESTAMP::text, 1, LOCALTIMESTAMP, 1, LOCALTIMESTAMP),
297
+ ('web.base.url', 'http://localhost:8069', 1, LOCALTIMESTAMP, 1, LOCALTIMESTAMP),
298
+ ('base.login_cooldown_after', '10', 1, LOCALTIMESTAMP, 1, LOCALTIMESTAMP),
299
+ ('base.login_cooldown_duration', '60', 1, LOCALTIMESTAMP, 1, LOCALTIMESTAMP);
300
+ END $$;
301
+ """
302
+
303
+ subprocess.run(
304
+ [
305
+ "psql",
306
+ "-h",
307
+ db_config["host"],
308
+ "-p",
309
+ db_config["port"],
310
+ "-U",
311
+ db_config["user"],
312
+ "-d",
313
+ db_name,
314
+ "-c",
315
+ sql,
316
+ ],
317
+ env=env,
318
+ )
319
+
320
+
321
+ def _run_odoo_cmd(
322
+ cfg: "ProjectConfig", args: list[str], check: bool = True
323
+ ) -> subprocess.CompletedProcess:
324
+ """Run an odoo-bin command in the project's venv."""
325
+ from odoo_dev.config import ProjectConfig
326
+
327
+ # Build command to run in venv
328
+ venv_python = cfg.venv_path / "bin" / "python"
329
+
330
+ cmd = [str(venv_python), str(cfg.odoo_bin), "-c", str(cfg.config_file), *args]
331
+
332
+ return subprocess.run(cmd, check=check)
@@ -0,0 +1,196 @@
1
+ """Docker container management commands (optional)."""
2
+
3
+ import subprocess
4
+
5
+ import typer
6
+
7
+ from odoo_dev.config import load_config
8
+ from odoo_dev.utils.console import error, success
9
+
10
+ app = typer.Typer(help="Docker container management (optional)")
11
+
12
+
13
+ @app.command()
14
+ def start() -> None:
15
+ """Start Odoo and PostgreSQL containers."""
16
+ cfg = load_config()
17
+ success(f"Starting Odoo ({cfg.odoo_version}) with Python {cfg.python_version}...")
18
+
19
+ # Check if odoo.conf exists
20
+ if not cfg.docker_config_file.exists():
21
+ error(f"Config file not found at {cfg.docker_config_file}")
22
+ error("Run 'odoo-dev setup' first to create the configuration.")
23
+ raise typer.Exit(1)
24
+
25
+ result = subprocess.run(
26
+ ["docker", "compose", "--project-name", cfg.project_name, "up", "-d"],
27
+ cwd=cfg.script_dir,
28
+ )
29
+ if result.returncode != 0:
30
+ error("Failed to start containers. Check the logs for more information.")
31
+ raise typer.Exit(1)
32
+
33
+ success("Odoo is starting up...")
34
+ success("Web interface: http://localhost:8069")
35
+ success("Debug port: localhost:5678")
36
+
37
+
38
+ @app.command()
39
+ def stop() -> None:
40
+ """Stop all containers."""
41
+ cfg = load_config()
42
+ success("Stopping containers...")
43
+ subprocess.run(
44
+ ["docker", "compose", "--project-name", cfg.project_name, "down"],
45
+ cwd=cfg.script_dir,
46
+ )
47
+
48
+
49
+ @app.command()
50
+ def restart() -> None:
51
+ """Restart all containers."""
52
+ stop()
53
+ start()
54
+
55
+
56
+ @app.command()
57
+ def logs() -> None:
58
+ """Show logs from the Odoo container."""
59
+ cfg = load_config()
60
+ success("Showing logs from Odoo container...")
61
+ subprocess.run(
62
+ ["docker", "compose", "--project-name", cfg.project_name, "logs", "-f", "odoo"],
63
+ cwd=cfg.script_dir,
64
+ )
65
+
66
+
67
+ @app.command()
68
+ def build(
69
+ community: bool = typer.Option(
70
+ False, "--community", help="Build for Community edition only"
71
+ ),
72
+ ) -> None:
73
+ """Rebuild the Odoo Docker image."""
74
+ import shutil
75
+
76
+ cfg = load_config()
77
+ success(f"Rebuilding Odoo image ({cfg.project_name}-odoo:{cfg.odoo_version})...")
78
+
79
+ # Create temporary build context
80
+ build_context = cfg.project_dir / f".odoo-build-{subprocess.os.getpid()}"
81
+ build_context.mkdir(parents=True, exist_ok=True)
82
+
83
+ try:
84
+ # Copy necessary files to build context
85
+ shutil.copy(cfg.script_dir / "Dockerfile", build_context)
86
+ shutil.copy(cfg.script_dir / "docker-entrypoint.sh", build_context)
87
+ shutil.copy(cfg.docker_config_file, build_context / "odoo.conf")
88
+
89
+ # Copy requirements files
90
+ if (cfg.project_dir / "requirements.txt").exists():
91
+ shutil.copy(cfg.project_dir / "requirements.txt", build_context)
92
+ if (cfg.project_dir / "odoo" / "requirements.txt").exists():
93
+ shutil.copy(
94
+ cfg.project_dir / "odoo" / "requirements.txt",
95
+ build_context / "odoo-requirements.txt",
96
+ )
97
+
98
+ # Create minimal docker-compose for build
99
+ compose_content = """name: ${PROJECT_NAME:-odoo}
100
+
101
+ services:
102
+ odoo:
103
+ image: ${PROJECT_NAME:-odoo-deploy}-odoo:${ODOO_VERSION:-17.0}
104
+ build:
105
+ context: .
106
+ dockerfile: Dockerfile
107
+ args:
108
+ - ODOO_VERSION=${ODOO_VERSION:-17.0}
109
+ - PYTHON_VERSION=${PYTHON_VERSION:-3.12}
110
+ platforms:
111
+ - linux/amd64
112
+ """
113
+ (build_context / "docker-compose.yml").write_text(compose_content)
114
+
115
+ # Build the image
116
+ env = subprocess.os.environ.copy()
117
+ env["PROJECT_NAME"] = cfg.project_name
118
+ env["ODOO_VERSION"] = cfg.odoo_version
119
+ env["PYTHON_VERSION"] = cfg.python_version
120
+
121
+ result = subprocess.run(
122
+ [
123
+ "docker",
124
+ "compose",
125
+ "build",
126
+ "--build-arg",
127
+ f"ODOO_VERSION={cfg.odoo_version}",
128
+ "--build-arg",
129
+ f"PYTHON_VERSION={cfg.python_version}",
130
+ "odoo",
131
+ ],
132
+ cwd=build_context,
133
+ env=env,
134
+ )
135
+
136
+ if result.returncode != 0:
137
+ error("Failed to build Docker image.")
138
+ raise typer.Exit(1)
139
+
140
+ success("Docker image built successfully!")
141
+
142
+ finally:
143
+ shutil.rmtree(build_context, ignore_errors=True)
144
+
145
+
146
+ @app.command()
147
+ def shell(
148
+ db_name: str = typer.Argument(..., help="Database name"),
149
+ ) -> None:
150
+ """Open an Odoo shell in the Docker container."""
151
+ cfg = load_config()
152
+
153
+ success(f"Opening Docker shell with database {db_name}...")
154
+
155
+ subprocess.run(
156
+ [
157
+ "docker",
158
+ "compose",
159
+ "--project-name",
160
+ cfg.project_name,
161
+ "exec",
162
+ "odoo",
163
+ "python",
164
+ "/opt/project/odoo/odoo-bin",
165
+ "shell",
166
+ "-d",
167
+ db_name,
168
+ "-c",
169
+ "/etc/odoo/odoo.conf",
170
+ "--no-http",
171
+ ],
172
+ cwd=cfg.script_dir,
173
+ )
174
+
175
+
176
+ @app.command()
177
+ def psql() -> None:
178
+ """Open a PostgreSQL shell in the Docker container."""
179
+ cfg = load_config()
180
+
181
+ success("Opening PostgreSQL shell...")
182
+ subprocess.run(
183
+ [
184
+ "docker",
185
+ "compose",
186
+ "--project-name",
187
+ cfg.project_name,
188
+ "exec",
189
+ "db",
190
+ "psql",
191
+ "-U",
192
+ "odoo",
193
+ "postgres",
194
+ ],
195
+ cwd=cfg.script_dir,
196
+ )