odoo-dev 0.3.3__tar.gz → 0.4.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.
Files changed (69) hide show
  1. odoo_dev-0.4.0/.github/workflows/release-please.yml +43 -0
  2. odoo_dev-0.4.0/.release-please-manifest.json +3 -0
  3. odoo_dev-0.4.0/CHANGELOG.md +8 -0
  4. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/PKG-INFO +24 -6
  5. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/README.md +23 -5
  6. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/pyproject.toml +1 -1
  7. odoo_dev-0.4.0/release-please-config.json +12 -0
  8. odoo_dev-0.4.0/src/odoo_dev/__init__.py +3 -0
  9. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/commands/run.py +38 -10
  10. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/commands/setup.py +22 -6
  11. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/config.py +50 -0
  12. odoo_dev-0.4.0/src/odoo_dev/preflight.py +158 -0
  13. odoo_dev-0.4.0/tests/test_db_config.py +49 -0
  14. odoo_dev-0.4.0/tests/test_preflight.py +132 -0
  15. odoo_dev-0.4.0/tests/test_setup_db.py +41 -0
  16. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/uv.lock +49 -1
  17. odoo_dev-0.3.3/.claude/settings.json +0 -8
  18. odoo_dev-0.3.3/.claude/settings.local.json +0 -13
  19. odoo_dev-0.3.3/src/odoo_dev/__init__.py +0 -3
  20. odoo_dev-0.3.3/tests/fixtures/odoo-empty/.gitignore +0 -78
  21. odoo_dev-0.3.3/tests/fixtures/odoo-empty/.gitlab-ci.yml +0 -8
  22. odoo_dev-0.3.3/tests/fixtures/odoo-empty/.gitmodules +0 -0
  23. odoo_dev-0.3.3/tests/fixtures/odoo-empty/.repos/.gitkeep +0 -0
  24. odoo_dev-0.3.3/tests/fixtures/odoo-empty/Notes/Warning Upgrade all +0 -45
  25. odoo_dev-0.3.3/tests/fixtures/odoo-empty/README.md +0 -0
  26. odoo_dev-0.3.3/tests/fixtures/odoo-empty/addons/.gitkeep +0 -0
  27. odoo_dev-0.3.3/tests/fixtures/odoo-empty/note.txt +0 -331
  28. odoo_dev-0.3.3/tests/fixtures/odoo-empty/requirements.txt +0 -0
  29. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/abstorelsymlink.sh +0 -12
  30. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/adaptation.md +0 -99
  31. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/after_migration.sh +0 -13
  32. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/checkout2 +0 -17
  33. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/cleanup.sql +0 -60
  34. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/deploy +0 -108
  35. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/fix_mail_templates.sql +0 -18
  36. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/gitpull +0 -17
  37. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/main.py +0 -569
  38. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/modules_cleanup.sql +0 -58
  39. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/post-upgrade steps.md +0 -78
  40. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/pre_commit_hook.py +0 -81
  41. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/requirements.txt +0 -3
  42. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/test +0 -62
  43. odoo_dev-0.3.3/tests/fixtures/odoo-empty/tools/update_addon_versions.py +0 -24
  44. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/.gitignore +0 -0
  45. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/.gitmodules +0 -0
  46. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/.python-version +0 -0
  47. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/cli.py +0 -0
  48. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/commands/__init__.py +0 -0
  49. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/commands/db.py +0 -0
  50. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/commands/docker.py +0 -0
  51. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/templates/__init__.py +0 -0
  52. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/templates/docker/Dockerfile +0 -0
  53. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/templates/docker/__init__.py +0 -0
  54. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/templates/docker/docker-entrypoint.sh +0 -0
  55. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/templates/vscode/__init__.py +0 -0
  56. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/templates/vscode/launch.json +0 -0
  57. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/templates/vscode/settings.json +0 -0
  58. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/templates/vscode/tasks.json +0 -0
  59. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/utils/__init__.py +0 -0
  60. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/src/odoo_dev/utils/console.py +0 -0
  61. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/__init__.py +0 -0
  62. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/fixtures/__init__.py +0 -0
  63. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/test_cli.py +0 -0
  64. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/test_config.py +0 -0
  65. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/test_db.py +0 -0
  66. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/test_drop_database.py +0 -0
  67. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/test_integration.py +0 -0
  68. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/test_run.py +0 -0
  69. {odoo_dev-0.3.3 → odoo_dev-0.4.0}/tests/test_setup.py +0 -0
@@ -0,0 +1,43 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ release-please:
13
+ runs-on: ubuntu-latest
14
+ outputs:
15
+ release_created: ${{ steps.release.outputs.release_created }}
16
+ tag_name: ${{ steps.release.outputs.tag_name }}
17
+ steps:
18
+ # Maintains a release PR from Conventional Commits; when that PR is merged
19
+ # it creates the git tag + GitHub release and sets release_created=true.
20
+ - uses: googleapis/release-please-action@v4
21
+ id: release
22
+ with:
23
+ token: ${{ secrets.GITHUB_TOKEN }}
24
+ # release-please-config.json and .release-please-manifest.json are
25
+ # read from the repo root by default.
26
+
27
+ publish:
28
+ needs: release-please
29
+ if: ${{ needs.release-please.outputs.release_created == 'true' }}
30
+ runs-on: ubuntu-latest
31
+ permissions:
32
+ contents: read
33
+ id-token: write # OIDC: mint a short-lived token for PyPI Trusted Publishing
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@v5
38
+ with:
39
+ python-version: "3.12"
40
+ - name: Build sdist + wheel
41
+ run: uv build
42
+ - name: Publish to PyPI (Trusted Publishing / OIDC)
43
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.4.0](https://github.com/bemade/odoo-dev/compare/v0.3.3...v0.4.0) (2026-06-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * configurable DB connection + connectivity preflight ([#3](https://github.com/bemade/odoo-dev/issues/3)) ([632738e](https://github.com/bemade/odoo-dev/commit/632738e37c1d22d79ec4a03cba72a28ca580f7c8))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-dev
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Odoo Development Environment Helper
5
5
  Project-URL: Homepage, https://git.bemade.org/bemade/odoo-dev
6
6
  Project-URL: Repository, https://git.bemade.org/bemade/odoo-dev
@@ -55,9 +55,10 @@ Docker at the end — answer "no" if you only want the local venv workflow.
55
55
  ## Database setup (read this before your first `run`/`test`)
56
56
 
57
57
  The generated `conf/odoo.conf` connects as PostgreSQL user **`odoo`** over the local
58
- socket. You need a running PostgreSQL server and a matching role. `setup` installs
59
- PostgreSQL but does **not** start it or create the role, so on a fresh machine do this
60
- once:
58
+ socket. You need a running PostgreSQL server and a matching role. `setup` never creates
59
+ the role, and what it installs differs by OS on **macOS** it installs the server (but
60
+ does **not** start it); on **Linux** it installs only the PostgreSQL *client* (so you
61
+ supply the server yourself). On a fresh machine do this once:
61
62
 
62
63
  **macOS (Homebrew):**
63
64
 
@@ -68,14 +69,23 @@ createuser -s odoo # create the role odoo.conf expects
68
69
  # export PATH="$(brew --prefix postgresql@18)/bin:$PATH"
69
70
  ```
70
71
 
71
- **Debian/Ubuntu:**
72
+ **Debian/Ubuntu:** install a server if you don't already have one (or point
73
+ `conf/odoo.conf` at an existing / remote / Docker PostgreSQL):
72
74
 
73
75
  ```bash
76
+ sudo apt-get install postgresql # if you don't already have a server
74
77
  sudo systemctl start postgresql
75
78
  sudo -u postgres createuser -s odoo
76
79
  ```
77
80
 
78
- You can change the DB user/password in `conf/odoo.conf` if you prefer a different role.
81
+ **Using a different / remote / Docker PostgreSQL:** set `DB_HOST`, `DB_PORT`,
82
+ `DB_USER`, `DB_PASSWORD` in `.env` before running `setup` (it writes them into
83
+ `conf/odoo.conf`), or edit `conf/odoo.conf` directly. With no `DB_HOST`, odoo-dev
84
+ connects over the local socket as the `odoo` role.
85
+
86
+ Before launching, `run`/`test`/`shell`/`update` run a quick connection preflight: if
87
+ the server is unreachable, the role is missing, or authentication fails, you get a
88
+ specific one-line fix instead of a stack trace.
79
89
 
80
90
  ## Commands
81
91
 
@@ -152,6 +162,14 @@ Create a `.env` file in your project root:
152
162
  ```bash
153
163
  ODOO_VERSION=19.0
154
164
  PYTHON_VERSION=3.12
165
+
166
+ # Optional — DB connection, written into conf/odoo.conf by `setup`.
167
+ # Omit DB_HOST/DB_PORT to use the local socket (the default). Set these to
168
+ # point at a remote / Docker / non-default PostgreSQL:
169
+ # DB_HOST=localhost
170
+ # DB_PORT=5432
171
+ # DB_USER=odoo
172
+ # DB_PASSWORD=odoo
155
173
  ```
156
174
 
157
175
  ## Requirements
@@ -41,9 +41,10 @@ Docker at the end — answer "no" if you only want the local venv workflow.
41
41
  ## Database setup (read this before your first `run`/`test`)
42
42
 
43
43
  The generated `conf/odoo.conf` connects as PostgreSQL user **`odoo`** over the local
44
- socket. You need a running PostgreSQL server and a matching role. `setup` installs
45
- PostgreSQL but does **not** start it or create the role, so on a fresh machine do this
46
- once:
44
+ socket. You need a running PostgreSQL server and a matching role. `setup` never creates
45
+ the role, and what it installs differs by OS on **macOS** it installs the server (but
46
+ does **not** start it); on **Linux** it installs only the PostgreSQL *client* (so you
47
+ supply the server yourself). On a fresh machine do this once:
47
48
 
48
49
  **macOS (Homebrew):**
49
50
 
@@ -54,14 +55,23 @@ createuser -s odoo # create the role odoo.conf expects
54
55
  # export PATH="$(brew --prefix postgresql@18)/bin:$PATH"
55
56
  ```
56
57
 
57
- **Debian/Ubuntu:**
58
+ **Debian/Ubuntu:** install a server if you don't already have one (or point
59
+ `conf/odoo.conf` at an existing / remote / Docker PostgreSQL):
58
60
 
59
61
  ```bash
62
+ sudo apt-get install postgresql # if you don't already have a server
60
63
  sudo systemctl start postgresql
61
64
  sudo -u postgres createuser -s odoo
62
65
  ```
63
66
 
64
- You can change the DB user/password in `conf/odoo.conf` if you prefer a different role.
67
+ **Using a different / remote / Docker PostgreSQL:** set `DB_HOST`, `DB_PORT`,
68
+ `DB_USER`, `DB_PASSWORD` in `.env` before running `setup` (it writes them into
69
+ `conf/odoo.conf`), or edit `conf/odoo.conf` directly. With no `DB_HOST`, odoo-dev
70
+ connects over the local socket as the `odoo` role.
71
+
72
+ Before launching, `run`/`test`/`shell`/`update` run a quick connection preflight: if
73
+ the server is unreachable, the role is missing, or authentication fails, you get a
74
+ specific one-line fix instead of a stack trace.
65
75
 
66
76
  ## Commands
67
77
 
@@ -138,6 +148,14 @@ Create a `.env` file in your project root:
138
148
  ```bash
139
149
  ODOO_VERSION=19.0
140
150
  PYTHON_VERSION=3.12
151
+
152
+ # Optional — DB connection, written into conf/odoo.conf by `setup`.
153
+ # Omit DB_HOST/DB_PORT to use the local socket (the default). Set these to
154
+ # point at a remote / Docker / non-default PostgreSQL:
155
+ # DB_HOST=localhost
156
+ # DB_PORT=5432
157
+ # DB_USER=odoo
158
+ # DB_PASSWORD=odoo
141
159
  ```
142
160
 
143
161
  ## Requirements
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "odoo-dev"
3
- version = "0.3.3"
3
+ version = "0.4.0"
4
4
  description = "Odoo Development Environment Helper"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "python",
6
+ "package-name": "odoo-dev",
7
+ "changelog-path": "CHANGELOG.md",
8
+ "include-component-in-tag": false,
9
+ "extra-files": ["src/odoo_dev/__init__.py"]
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ """Odoo Development Environment Helper."""
2
+
3
+ __version__ = "0.4.0" # x-release-please-version
@@ -10,6 +10,7 @@ from typing import Annotated
10
10
  import typer
11
11
 
12
12
  from odoo_dev.config import load_config
13
+ from odoo_dev.preflight import require_db
13
14
  from odoo_dev.utils.console import error, info, success, warning
14
15
 
15
16
 
@@ -53,6 +54,8 @@ def run(
53
54
  error("Run 'odoo-dev setup' first to clone Odoo repositories.")
54
55
  raise typer.Exit(1)
55
56
 
57
+ require_db(cfg)
58
+
56
59
  venv_python = cfg.venv_path / "bin" / "python"
57
60
 
58
61
  # Build command
@@ -114,6 +117,8 @@ def shell(
114
117
  error("Run 'odoo-dev setup' first.")
115
118
  raise typer.Exit(1)
116
119
 
120
+ require_db(cfg)
121
+
117
122
  venv_python = cfg.venv_path / "bin" / "python"
118
123
 
119
124
  success(f"Opening Odoo shell with database {db_name}...")
@@ -148,6 +153,8 @@ def update(
148
153
  error("Run 'odoo-dev setup' first.")
149
154
  raise typer.Exit(1)
150
155
 
156
+ require_db(cfg)
157
+
151
158
  venv_python = cfg.venv_path / "bin" / "python"
152
159
 
153
160
  success(f"Updating modules: {modules} on database {db_name}...")
@@ -192,7 +199,8 @@ def test(
192
199
  bool, typer.Option("--coverage/--no-coverage", help="Enable coverage reporting")
193
200
  ] = True,
194
201
  keep_db: Annotated[
195
- bool, typer.Option("--keep-db/--no-keep-db", help="Keep test database after tests")
202
+ bool,
203
+ typer.Option("--keep-db/--no-keep-db", help="Keep test database after tests"),
196
204
  ] = False,
197
205
  exclude: Annotated[
198
206
  str | None,
@@ -254,6 +262,8 @@ def test(
254
262
  if db_name is None:
255
263
  db_name = f"test_{int(time.time())}"
256
264
 
265
+ require_db(cfg)
266
+
257
267
  venv_python = cfg.venv_path / "bin" / "python"
258
268
 
259
269
  # Determine modules to test
@@ -291,7 +301,15 @@ def test(
291
301
  if not clone_dir.exists():
292
302
  branch = os.environ.get("ODOO_VERSION", cfg.odoo_version)
293
303
  result = subprocess.run(
294
- ["git", "clone", "--depth=1", "-b", branch, repo_url, str(clone_dir)],
304
+ [
305
+ "git",
306
+ "clone",
307
+ "--depth=1",
308
+ "-b",
309
+ branch,
310
+ repo_url,
311
+ str(clone_dir),
312
+ ],
295
313
  capture_output=True,
296
314
  )
297
315
  if result.returncode != 0:
@@ -370,7 +388,9 @@ def test(
370
388
 
371
389
  if coverage:
372
390
  # Build coverage source paths
373
- coverage_source = ",".join(str(cfg.addons_dir / m) for m in modules.split(","))
391
+ coverage_source = ",".join(
392
+ str(cfg.addons_dir / m) for m in modules.split(",")
393
+ )
374
394
 
375
395
  test_cmd = [
376
396
  str(venv_python),
@@ -512,9 +532,12 @@ def _drop_database(cfg, db_name: str) -> None:
512
532
  result = subprocess.run(
513
533
  [
514
534
  "dropdb",
515
- "-h", db_config["host"],
516
- "-p", db_config["port"],
517
- "-U", db_config["user"],
535
+ "-h",
536
+ db_config["host"],
537
+ "-p",
538
+ db_config["port"],
539
+ "-U",
540
+ db_config["user"],
518
541
  "--if-exists",
519
542
  db_name,
520
543
  ],
@@ -534,10 +557,14 @@ def _terminate_connections(db_name: str, db_config: dict[str, str]) -> None:
534
557
  subprocess.run(
535
558
  [
536
559
  "psql",
537
- "-h", db_config["host"],
538
- "-p", db_config["port"],
539
- "-U", db_config["user"],
540
- "-d", "postgres",
560
+ "-h",
561
+ db_config["host"],
562
+ "-p",
563
+ db_config["port"],
564
+ "-U",
565
+ db_config["user"],
566
+ "-d",
567
+ "postgres",
541
568
  "-c",
542
569
  f"SELECT pg_terminate_backend(pid) FROM pg_stat_activity "
543
570
  f"WHERE datname = '{db_name}' AND pid <> pg_backend_pid()",
@@ -547,6 +574,7 @@ def _terminate_connections(db_name: str, db_config: dict[str, str]) -> None:
547
574
  env=env,
548
575
  )
549
576
 
577
+
550
578
  def _get_addons_path(config_file) -> str:
551
579
  """Extract addons_path from odoo.conf."""
552
580
  for line in config_file.read_text().splitlines():
@@ -309,12 +309,28 @@ def _setup_odoo_config(cfg, community_only: bool = False) -> None:
309
309
  # Filter to only existing paths
310
310
  addons_paths = [p for p in addons_paths if Path(p).exists()]
311
311
 
312
- config_content = f"""[options]
313
- addons_path = {",".join(addons_paths)}
314
- admin_passwd = admin
315
- db_user = odoo
316
- db_password = odoo
317
- """
312
+ # DB connection is configurable via .env (loaded into the environment by
313
+ # load_config). Host/port are written only when set, so the default is the
314
+ # local socket — matching odoo-bin's behavior when db_host is absent.
315
+ db_user = os.getenv("DB_USER", "odoo")
316
+ db_password = os.getenv("DB_PASSWORD", "odoo")
317
+ db_host = os.getenv("DB_HOST", "")
318
+ db_port = os.getenv("DB_PORT", "")
319
+
320
+ db_lines = []
321
+ if db_host:
322
+ db_lines.append(f"db_host = {db_host}")
323
+ if db_port:
324
+ db_lines.append(f"db_port = {db_port}")
325
+ db_lines.append(f"db_user = {db_user}")
326
+ db_lines.append(f"db_password = {db_password}")
327
+
328
+ config_content = (
329
+ "[options]\n"
330
+ f"addons_path = {','.join(addons_paths)}\n"
331
+ "admin_passwd = admin\n"
332
+ f"{chr(10).join(db_lines)}\n"
333
+ )
318
334
 
319
335
  conf_file.write_text(config_content)
320
336
  conf_file.chmod(0o600)
@@ -9,6 +9,56 @@ DEFAULT_ODOO_VERSION = "19.0"
9
9
  DEFAULT_PYTHON_VERSION = "3.12"
10
10
 
11
11
 
12
+ @dataclass
13
+ class DbConfig:
14
+ """PostgreSQL connection settings, as odoo-bin will use them.
15
+
16
+ An empty ``host``/``port`` means "use the libpq defaults" (i.e. the local
17
+ Unix socket), which is exactly what odoo-bin does when ``db_host`` is absent
18
+ from odoo.conf. The preflight check must mirror that, so we keep them empty
19
+ rather than substituting ``localhost``/``5432`` (which would force TCP and
20
+ can spuriously fail on socket/peer-auth setups).
21
+ """
22
+
23
+ host: str = ""
24
+ port: str = ""
25
+ user: str = "odoo"
26
+ password: str = ""
27
+ name: str | None = None
28
+
29
+
30
+ def read_db_config(config_file: Path) -> "DbConfig":
31
+ """Read DB connection settings from an odoo.conf file.
32
+
33
+ Args:
34
+ config_file: Path to odoo.conf. A missing file yields defaults.
35
+
36
+ Returns:
37
+ DbConfig reflecting the connection odoo-bin would make.
38
+ """
39
+ db = DbConfig()
40
+ if not config_file.exists():
41
+ return db
42
+
43
+ for line in config_file.read_text().splitlines():
44
+ line = line.strip()
45
+ if "=" not in line or line.startswith("#"):
46
+ continue
47
+ key, _, value = line.partition("=")
48
+ key, value = key.strip(), value.strip()
49
+ if key == "db_host":
50
+ db.host = value
51
+ elif key == "db_port":
52
+ db.port = value
53
+ elif key == "db_user":
54
+ db.user = value
55
+ elif key == "db_password":
56
+ db.password = value
57
+ elif key == "db_name":
58
+ db.name = value
59
+ return db
60
+
61
+
12
62
  @dataclass
13
63
  class ProjectConfig:
14
64
  """Configuration for an Odoo development project."""
@@ -0,0 +1,158 @@
1
+ """Fast PostgreSQL connectivity preflight for commands that need a database.
2
+
3
+ odoo-dev does not own the PostgreSQL server lifecycle (you bring your own:
4
+ a local service, Docker, or a remote/managed DB). Instead, commands that need
5
+ a database call :func:`require_db` first, which connects exactly the way
6
+ odoo-bin will and, on failure, prints a specific, actionable diagnostic rather
7
+ than letting Odoo fail later with a noisy stack trace.
8
+ """
9
+
10
+ import os
11
+ import subprocess
12
+ from dataclasses import dataclass
13
+
14
+ import typer
15
+
16
+ from odoo_dev.config import DbConfig, read_db_config
17
+ from odoo_dev.utils.console import error, warning
18
+
19
+
20
+ @dataclass
21
+ class PreflightResult:
22
+ """Outcome of a connectivity check."""
23
+
24
+ ok: bool
25
+ category: str # ok | client_missing | unreachable | auth | role_missing | db_missing | unknown
26
+ message: str # raw stderr (or short note), for context
27
+
28
+
29
+ def psql_argv(
30
+ db: DbConfig, dbname: str = "postgres", sql: str = "SELECT 1"
31
+ ) -> list[str]:
32
+ """Build a psql argv that connects the way odoo-bin would.
33
+
34
+ Host/port are only passed when set, so an unset host uses the local socket
35
+ (libpq default) rather than forcing TCP to localhost.
36
+ """
37
+ # -w / --no-password: never prompt interactively (would hang a preflight);
38
+ # fail fast instead so we can classify the failure.
39
+ argv = ["psql", "-w"]
40
+ if db.host:
41
+ argv += ["-h", db.host]
42
+ if db.port:
43
+ argv += ["-p", db.port]
44
+ argv += ["-U", db.user, "-d", dbname, "-tAc", sql]
45
+ return argv
46
+
47
+
48
+ def classify_psql_failure(returncode: int, stderr: str) -> str:
49
+ """Map a failed psql attempt to a category for actionable guidance."""
50
+ s = stderr.lower()
51
+ # Order matters: the "does not exist" variants must be checked before the
52
+ # generic connection failures.
53
+ if "does not exist" in s and "role" in s:
54
+ return "role_missing"
55
+ if "does not exist" in s and "database" in s:
56
+ return "db_missing"
57
+ if "authentication failed" in s or "no password supplied" in s:
58
+ return "auth"
59
+ if (
60
+ "could not connect" in s
61
+ or "connection refused" in s
62
+ or "could not translate host" in s
63
+ or "no such file or directory" in s
64
+ or "is the server running" in s
65
+ ):
66
+ return "unreachable"
67
+ return "unknown"
68
+
69
+
70
+ def check_db_connection(db: DbConfig, dbname: str = "postgres") -> PreflightResult:
71
+ """Attempt a trivial query and report whether (and why) it failed."""
72
+ env = os.environ.copy()
73
+ if db.password:
74
+ env["PGPASSWORD"] = db.password
75
+ try:
76
+ proc = subprocess.run(
77
+ psql_argv(db, dbname=dbname),
78
+ capture_output=True,
79
+ text=True,
80
+ env=env,
81
+ )
82
+ except FileNotFoundError:
83
+ return PreflightResult(
84
+ False,
85
+ "client_missing",
86
+ "psql executable not found on PATH",
87
+ )
88
+ if proc.returncode == 0:
89
+ return PreflightResult(True, "ok", "")
90
+ return PreflightResult(
91
+ False,
92
+ classify_psql_failure(proc.returncode, proc.stderr),
93
+ proc.stderr.strip(),
94
+ )
95
+
96
+
97
+ def _target(db: DbConfig) -> str:
98
+ """Human-readable connection target for messages."""
99
+ where = f"{db.host}:{db.port or '5432'}" if db.host else "the local socket"
100
+ return f"PostgreSQL as user '{db.user}' on {where}"
101
+
102
+
103
+ def _guidance(category: str, db: DbConfig) -> str:
104
+ """Actionable next step for a failure category."""
105
+ if category == "client_missing":
106
+ return (
107
+ "The psql client isn't on your PATH. Install the PostgreSQL client "
108
+ "(macOS: `brew install libpq` and add it to PATH; Debian/Ubuntu: "
109
+ "`sudo apt-get install postgresql-client`)."
110
+ )
111
+ if category == "unreachable":
112
+ return (
113
+ "Could not reach a PostgreSQL server. Start one (macOS: "
114
+ "`brew services start postgresql@18`; Debian/Ubuntu: "
115
+ "`sudo systemctl start postgresql`), or set db_host/db_port in "
116
+ "conf/odoo.conf (or DB_HOST/DB_PORT in .env) to point at an "
117
+ "existing / remote / Docker PostgreSQL."
118
+ )
119
+ if category == "auth":
120
+ return (
121
+ f"Authentication failed for role '{db.user}'. Check db_user/"
122
+ "db_password in conf/odoo.conf, or adjust the server's pg_hba.conf "
123
+ "(peer vs. password auth). A ~/.pgpass entry can supply the password."
124
+ )
125
+ if category == "role_missing":
126
+ return (
127
+ f"The role '{db.user}' does not exist. Create it once: "
128
+ f"`createuser -s {db.user}` (Debian/Ubuntu: prefix with "
129
+ "`sudo -u postgres`)."
130
+ )
131
+ if category == "db_missing":
132
+ return (
133
+ "The maintenance database is missing — unusual; your PostgreSQL "
134
+ "install may be incomplete."
135
+ )
136
+ return "See the PostgreSQL error above."
137
+
138
+
139
+ def require_db(cfg, dbname: str = "postgres") -> None:
140
+ """Ensure the configured database server is reachable, or exit with guidance.
141
+
142
+ Args:
143
+ cfg: ProjectConfig (only ``config_file`` is used).
144
+ dbname: Database to connect to for the check (default: ``postgres``).
145
+
146
+ Raises:
147
+ typer.Exit: if the server cannot be reached / authenticated.
148
+ """
149
+ db = read_db_config(cfg.config_file)
150
+ result = check_db_connection(db, dbname=dbname)
151
+ if result.ok:
152
+ return
153
+
154
+ error(f"Cannot connect to {_target(db)}.")
155
+ if result.message:
156
+ warning(result.message)
157
+ error(_guidance(result.category, db))
158
+ raise typer.Exit(1)
@@ -0,0 +1,49 @@
1
+ """Tests for reading DB connection settings the way odoo-bin will use them."""
2
+
3
+ from pathlib import Path
4
+
5
+ from odoo_dev.config import DbConfig, read_db_config
6
+
7
+
8
+ class TestReadDbConfig:
9
+ def test_parses_full_config(self, tmp_path: Path):
10
+ conf = tmp_path / "odoo.conf"
11
+ conf.write_text(
12
+ "[options]\n"
13
+ "db_host = db.example.com\n"
14
+ "db_port = 5544\n"
15
+ "db_user = appuser\n"
16
+ "db_password = secret\n"
17
+ "db_name = mydb\n"
18
+ )
19
+ db = read_db_config(conf)
20
+ assert db == DbConfig(
21
+ host="db.example.com",
22
+ port="5544",
23
+ user="appuser",
24
+ password="secret",
25
+ name="mydb",
26
+ )
27
+
28
+ def test_unset_host_port_stay_empty_for_socket(self, tmp_path: Path):
29
+ # odoo-bin with no db_host uses the local socket; preflight must mirror that
30
+ conf = tmp_path / "odoo.conf"
31
+ conf.write_text("[options]\ndb_user = odoo\ndb_password = odoo\n")
32
+ db = read_db_config(conf)
33
+ assert db.host == ""
34
+ assert db.port == ""
35
+ assert db.user == "odoo"
36
+ assert db.password == "odoo"
37
+ assert db.name is None
38
+
39
+ def test_defaults_when_file_missing(self, tmp_path: Path):
40
+ db = read_db_config(tmp_path / "nonexistent.conf")
41
+ assert db == DbConfig()
42
+ assert db.user == "odoo"
43
+
44
+ def test_handles_whitespace(self, tmp_path: Path):
45
+ conf = tmp_path / "odoo.conf"
46
+ conf.write_text("db_user = spacey \ndb_host=nospace\n")
47
+ db = read_db_config(conf)
48
+ assert db.user == "spacey"
49
+ assert db.host == "nospace"