odoo-installer 0.1.1__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 (64) hide show
  1. odoo_installer-0.1.1/.gitignore +26 -0
  2. odoo_installer-0.1.1/CHANGELOG.md +103 -0
  3. odoo_installer-0.1.1/DEVELOPMENT.md +465 -0
  4. odoo_installer-0.1.1/LICENSE +21 -0
  5. odoo_installer-0.1.1/PKG-INFO +130 -0
  6. odoo_installer-0.1.1/README.md +92 -0
  7. odoo_installer-0.1.1/pyproject.toml +110 -0
  8. odoo_installer-0.1.1/src/odoo_installer/__init__.py +5 -0
  9. odoo_installer-0.1.1/src/odoo_installer/__main__.py +8 -0
  10. odoo_installer-0.1.1/src/odoo_installer/adapters/__init__.py +5 -0
  11. odoo_installer-0.1.1/src/odoo_installer/adapters/docker.py +189 -0
  12. odoo_installer-0.1.1/src/odoo_installer/adapters/filesystem.py +88 -0
  13. odoo_installer-0.1.1/src/odoo_installer/adapters/git.py +88 -0
  14. odoo_installer-0.1.1/src/odoo_installer/adapters/github.py +91 -0
  15. odoo_installer-0.1.1/src/odoo_installer/adapters/system.py +114 -0
  16. odoo_installer-0.1.1/src/odoo_installer/cli/__init__.py +0 -0
  17. odoo_installer-0.1.1/src/odoo_installer/cli/common.py +51 -0
  18. odoo_installer-0.1.1/src/odoo_installer/cli/config.py +105 -0
  19. odoo_installer-0.1.1/src/odoo_installer/cli/db.py +140 -0
  20. odoo_installer-0.1.1/src/odoo_installer/cli/deps.py +54 -0
  21. odoo_installer-0.1.1/src/odoo_installer/cli/doctor.py +36 -0
  22. odoo_installer-0.1.1/src/odoo_installer/cli/install.py +45 -0
  23. odoo_installer-0.1.1/src/odoo_installer/cli/instance.py +267 -0
  24. odoo_installer-0.1.1/src/odoo_installer/cli/main.py +65 -0
  25. odoo_installer-0.1.1/src/odoo_installer/cli/module.py +399 -0
  26. odoo_installer-0.1.1/src/odoo_installer/cli/test.py +180 -0
  27. odoo_installer-0.1.1/src/odoo_installer/config.py +152 -0
  28. odoo_installer-0.1.1/src/odoo_installer/console.py +157 -0
  29. odoo_installer-0.1.1/src/odoo_installer/constants.py +27 -0
  30. odoo_installer-0.1.1/src/odoo_installer/core/__init__.py +1 -0
  31. odoo_installer-0.1.1/src/odoo_installer/core/dbms.py +168 -0
  32. odoo_installer-0.1.1/src/odoo_installer/core/instances.py +511 -0
  33. odoo_installer-0.1.1/src/odoo_installer/core/modules.py +572 -0
  34. odoo_installer-0.1.1/src/odoo_installer/core/plan.py +37 -0
  35. odoo_installer-0.1.1/src/odoo_installer/core/prereqs.py +209 -0
  36. odoo_installer-0.1.1/src/odoo_installer/core/runner.py +41 -0
  37. odoo_installer-0.1.1/src/odoo_installer/core/stack.py +83 -0
  38. odoo_installer-0.1.1/src/odoo_installer/core/tester.py +166 -0
  39. odoo_installer-0.1.1/src/odoo_installer/exceptions.py +53 -0
  40. odoo_installer-0.1.1/src/odoo_installer/py.typed +0 -0
  41. odoo_installer-0.1.1/src/odoo_installer/schemas.py +145 -0
  42. odoo_installer-0.1.1/src/odoo_installer/templates/.env.j2 +7 -0
  43. odoo_installer-0.1.1/src/odoo_installer/templates/docker-compose.yml.j2 +41 -0
  44. odoo_installer-0.1.1/src/odoo_installer/templates/odoo.conf.j2 +11 -0
  45. odoo_installer-0.1.1/tests/unit/fakes.py +277 -0
  46. odoo_installer-0.1.1/tests/unit/test_adopt.py +146 -0
  47. odoo_installer-0.1.1/tests/unit/test_cli.py +47 -0
  48. odoo_installer-0.1.1/tests/unit/test_cli_config.py +124 -0
  49. odoo_installer-0.1.1/tests/unit/test_cli_db.py +184 -0
  50. odoo_installer-0.1.1/tests/unit/test_cli_doctor.py +93 -0
  51. odoo_installer-0.1.1/tests/unit/test_cli_install.py +77 -0
  52. odoo_installer-0.1.1/tests/unit/test_cli_instance.py +174 -0
  53. odoo_installer-0.1.1/tests/unit/test_cli_module.py +304 -0
  54. odoo_installer-0.1.1/tests/unit/test_cli_test.py +175 -0
  55. odoo_installer-0.1.1/tests/unit/test_config.py +178 -0
  56. odoo_installer-0.1.1/tests/unit/test_dbms.py +96 -0
  57. odoo_installer-0.1.1/tests/unit/test_exceptions.py +26 -0
  58. odoo_installer-0.1.1/tests/unit/test_filesystem.py +118 -0
  59. odoo_installer-0.1.1/tests/unit/test_instances.py +277 -0
  60. odoo_installer-0.1.1/tests/unit/test_modules.py +397 -0
  61. odoo_installer-0.1.1/tests/unit/test_plan.py +67 -0
  62. odoo_installer-0.1.1/tests/unit/test_prereqs.py +117 -0
  63. odoo_installer-0.1.1/tests/unit/test_stack.py +62 -0
  64. odoo_installer-0.1.1/tests/unit/test_tester.py +151 -0
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+
11
+ # Tooling caches
12
+ .mypy_cache/
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ coverage.xml
17
+ htmlcov/
18
+
19
+ # Editors / OS
20
+ .idea/
21
+ .vscode/
22
+ *.swp
23
+ .DS_Store
24
+
25
+ # Project
26
+ *.log
@@ -0,0 +1,103 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.1] - 2026-09-01
9
+
10
+ ### Fixed
11
+
12
+ - DEVELOPMENT.md synced with the v0.1.0 implementation state: composition root
13
+ (`cli/deps.py`) and `core/plan.py` added to the package layout, real schema model
14
+ names, the `.env`/compose template contents in §4, `tested.toml` in the state model,
15
+ and the removed `--debug` claim.
16
+ - The deferred integration test layer and the CI docker job are now recorded as a v1.1
17
+ roadmap item with a v1.1 priority list.
18
+ - README install instructions no longer claim PyPI availability for the unpublished
19
+ 0.1.0 release.
20
+ - Removed the stale `--debug` traceback claim from the exceptions module docstring.
21
+
22
+ ### Added
23
+
24
+ - First publication to PyPI (`pip install odoo-installer`).
25
+
26
+ ## [0.1.0] - 2026-08-31
27
+
28
+ ### Added
29
+
30
+ - Project scaffold: src-layout package, Typer CLI entry point (`odoo-installer`, alias
31
+ `oii`), `--version` flag and `version` command, static constants for the Odoo 19.0
32
+ stack, and the typed error hierarchy.
33
+ - Developer tooling: ruff (format + lint), mypy (strict), pytest with coverage, and
34
+ unit tests for the CLI entry point.
35
+ - CI: lint/types and unit test matrix (Python 3.11–3.13) via GitHub Actions.
36
+ - `doctor` command: host checks for the docker engine and compose plugin, docker group
37
+ membership (read from `/etc/group`, not stale process groups), git, disk space at the
38
+ instances root, port availability on 8069–8099, and GitHub API reachability. Renders a
39
+ rich table or `--json`; exits with code 4 when a critical check fails.
40
+ - `config show|set|path` sub-app backed by validated, atomic TOML persistence
41
+ (`~/.config/odoo-installer/config.toml`); instance registry load/save helpers for M2.
42
+ - Host adapters (docker, system, github, filesystem) behind `Protocol` interfaces;
43
+ unit tests run fully offline against fakes.
44
+ - `install` command: plan-first host prerequisite installation (docker engine, compose
45
+ plugin, git) via pacman/apt with `--apply`; idempotent — satisfied hosts are no-ops.
46
+ - `instance` sub-app: `create` (dry-run plan → `--apply`, auto port allocation in the
47
+ configured range, jinja-rendered `docker-compose.yml`/`.env`/`odoo.conf`, generated
48
+ secrets persisted across re-runs, `docker compose up -d` + health wait),
49
+ `list`, `show`, `start`, `stop`, `restart`, and `remove` (dry-run by default;
50
+ execution requires `--apply --yes`; `--remove-data` destroys the pgdata volume).
51
+ - Instance state: registry (`registry.toml`) plus per-instance manifest
52
+ (`.odoo-installer.json`); `create` re-runs are idempotent and pin the recorded port.
53
+ - Docker adapter additions: `compose`, `wait_healthy` (with log capture on failure),
54
+ `logs`; system adapter package/service operations; filesystem adapter atomic
55
+ writes with permission modes (`.env` 0600, `odoo.conf` 0644 for container readability).
56
+ - `instance adopt <dir>`: register an existing compose stack (detected purely from
57
+ container labels — no compose file parsing) and manage it read-mostly: `start` uses
58
+ `docker compose start` so adopted stacks are never recreated, and no stack files are
59
+ rewritten (only the odoo-installer manifest is added).
60
+ - `db` sub-app: `list` (sizes via `pg_database_size`), `create` (idempotent),
61
+ `drop` and `reset` — executed through `psql` in the db container. Database names are
62
+ always explicit CLI arguments; `postgres`/`template0`/`template1` refuse to be
63
+ dropped; `drop`/`reset` are plan-first and execute only with `--apply --yes`.
64
+ - `module` sub-app for OCA repositories and modules:
65
+ `add` verifies the 19.0 branch via the GitHub API before cloning (never guesses —
66
+ a repo whose default branch is 18.0 but has 19.0 is handled), clones into the
67
+ instance's `repos/` (or the configured `repo_root` for adopted stacks), supports
68
+ `--sparse` and mounting existing checkouts unmutated (`--repo`), appends the compose
69
+ volume + `addons_path` with automatic backups and `docker compose config`
70
+ validation, and restarts web only for stacks the CLI created (adopted stacks get a
71
+ "restart with your own tooling" report instead);
72
+ `list` shows modules with per-database install states (`--db`, `--json`);
73
+ `search` queries the OCA GitHub org;
74
+ `install`/`upgrade` run `odoo -i/-u --stop-after-init --http-port=8071` inside the
75
+ web container against an explicit `--db` (scratch `oitest_*` recommended) and verify
76
+ the resulting `ir_module_module` states;
77
+ `remove` unmounts, optionally resets module states and purges the clone.
78
+ - `filesystem.write_text` now preserves an existing file's permission mode across
79
+ atomic replacement — edits of container-mounted configs (odoo.conf) no longer
80
+ accidentally become unreadable to the container's odoo user.
81
+ - `module test <name>`: installs the module on a throwaway `oitest_<module>`
82
+ database, runs `--test-enable --test-tags=/<module>` inside the web container,
83
+ captures the full log (XDG state dir for adopted stacks), and prints PASS/FAIL
84
+ (exit 3 on failure). A PASS is recorded in the installable-addons whitelist
85
+ (`~/.config/odoo-installer/tested.toml`) with repo, branch, commit and log path.
86
+ - Installable-addons whitelist enforcement: `module install`/`upgrade` refuse
87
+ untested modules unless `--allow-untested` is passed; `module list` shows a
88
+ Tested column.
89
+ - Shallow single-branch clones (`--depth 1 --branch 19.0`) for owned clones —
90
+ big repos like OCA/web now cost ~22 MB instead of hundreds.
91
+ - `config edit`: opens `config.toml` in `$EDITOR`, validates the result before
92
+ saving, and refuses invalid edits (nothing is written).
93
+ - `test suite`: batch-tests every module on an instance's addons_path (filter by
94
+ `--only <repo>` or `--modules m1,m2`), one scratch DB per module, sequential;
95
+ PASSes feed the whitelist; Markdown/JSON reports with `--output` (repeatable,
96
+ e.g. `--output report.md --output report.json`); rich summary; exit 3 on any
97
+ failure; `--keep-db` keeps scratch databases.
98
+ - Test failure classification: logs are parsed into failure kinds (test failure,
99
+ import error, not installable, addons_path, manifest, traceback, exit code).
100
+ - Test tooling: `tests/unit/test_filesystem.py` covers the filesystem adapter's
101
+ mode-preservation semantics directly; the docker/git/github/system adapters
102
+ (thin subprocess/network wrappers exercised live) are omitted from coverage,
103
+ which is pinned at ≥ 85%.
@@ -0,0 +1,465 @@
1
+ # DEVELOPMENT.md — `odoo-installer`
2
+
3
+ Development guide for **odoo-installer**: a professional, pip-installable Python CLI that
4
+ installs, configures, and manages **Odoo 19.0 Docker stacks** and their modules — including
5
+ correct-branch OCA module management and automated installability testing of every core and
6
+ OCA module.
7
+
8
+ This document is the source of truth for architecture, scope, conventions, and milestones.
9
+ Any change to an approved decision must be reflected here first.
10
+
11
+ ---
12
+
13
+ ## 1. Approved decisions
14
+
15
+ Locked in with the project owner before implementation:
16
+
17
+ | # | Decision | Choice |
18
+ |---|----------|--------|
19
+ | D1 | Runtime model | **Docker only.** The CLI never installs Odoo natively. It generates and manages `docker compose` stacks (web + db) and runs all Odoo commands inside containers. |
20
+ | D2 | Odoo version | **19.0 only** for v1. The version is a constant (`ODOO_VERSION = "19.0"`), not a user-facing matrix. Default image: `odoo:19.0` (Docker Hub, active; dated tags like `19.0-20260817` exist). |
21
+ | D3 | Host OS support | **Arch first** (pacman adapter, tested for real on this machine), **Debian/Ubuntu next** (apt adapter in a later milestone). |
22
+ | D4 | Language & packaging | Python `>= 3.11`, src layout, `hatchling` build backend, console script `odoo-installer` (+ short alias `oii`), developed and used from a local `.venv`. |
23
+ | D5 | CLI framework | **Typer + Rich** (typed commands, tables, shell completion). |
24
+ | D6 | Config | **TOML**: global `~/.config/odoo-installer/config.toml`, per-instance manifest `<stack>/.odoo-installer.json`, global registry `~/.config/odoo-installer/registry.toml`. |
25
+ | D7 | Safety | System-changing and destructive commands are **plan-first**: without `--apply` / `--yes` they print exactly what they would do and exit 0. Idempotent re-runs are a requirement. |
26
+ | D8 | Git access | Plain `git` via `subprocess` (no GitPython). GitHub metadata via **httpx**. |
27
+
28
+ ### Non-goals for v1
29
+
30
+ - Native (systemd / non-Docker) Odoo installation.
31
+ - Odoo versions other than 19.0 (the code keeps the version a single constant so later support is cheap).
32
+ - GUI / TUI.
33
+ - Database backup/restore, SMTP setup wizard, reverse-proxy/TLS generation (future roadmap).
34
+
35
+ ---
36
+
37
+ ## 2. Product scope — command surface
38
+
39
+ ```text
40
+ odoo-installer doctor [--json]
41
+ Host diagnostics: docker engine, compose plugin, git, disk space, port conflicts,
42
+ github.com reachability, user-in-docker-group. Exit code 4 when a critical check fails.
43
+
44
+ odoo-installer install [--apply]
45
+ Install HOST PREREQUISITES only (docker engine, compose plugin, git) via pacman/apt.
46
+ Never installs Odoo itself — that is what the stack is for.
47
+
48
+ odoo-installer instance create <name> [--dir PATH] [--http-port N] [--image TAG] [--pg-tag N] [--apply]
49
+ Render a complete compose stack (compose file, .env, config/odoo.conf), `up -d`,
50
+ wait for /web/health, register the instance.
51
+ odoo-installer instance list | show <name>
52
+ odoo-installer instance start|stop|restart <name>
53
+ odoo-installer instance remove <name> [--remove-data] [--yes]
54
+ remove defaults to keeping volumes/DBs; --remove-data destroys the pgdata volume.
55
+ odoo-installer instance adopt <dir>
56
+ Register an EXISTING compose stack (e.g. ~/Projects/odoo-docker) without rewriting it.
57
+ Adopted stacks are managed read-mostly: exec/psql/logs are allowed; file rewriting is not.
58
+
59
+ odoo-installer module add <oca-repo> [--modules m1,m2] [--sparse] [--repo PATH] [--apply]
60
+ Clone OCA/<repo> at the branch matching 19.0, mount it into the stack, rewrite
61
+ addons_path, restart web. --repo mounts an existing local checkout instead of cloning.
62
+ odoo-installer module list [--instance NAME] [--json]
63
+ odoo-installer module search <query>
64
+ GitHub API: find OCA repos and the modules they contain for 19.0.
65
+ odoo-installer module install <name...> [--db DB]
66
+ odoo-installer module upgrade <name...> [--db DB]
67
+ Run `odoo -d <db> -i/-u <name> --stop-after-init --http-port=8071` inside the web container.
68
+ odoo-installer module remove <name...> [--db DB] [--purge-repo] [--yes]
69
+
70
+ odoo-installer db list [--instance] | create <db> [--instance]
71
+ The database name is always an explicit positional argument, never a default.
72
+ odoo-installer db drop|reset <db> [--instance] [--yes] [--apply]
73
+ Executed through psql in the db container. drop/reset always require --yes.
74
+
75
+ odoo-installer module test <name> [--instance] [--keep-db]
76
+ Install the module on a throwaway DB (oitest_<module>_<ts>), run its tests
77
+ (--test-enable --test-tags /<module>), capture and parse the log, print PASS/FAIL.
78
+ odoo-installer test suite [--instance <name>] [--only <repo>] [--modules m1,m2]
79
+ [--output report.{md,json}] [--keep-db]
80
+ Batch over the modules of every repo on the stack's addons_path (filter by
81
+ repo or module list); one scratch DB (oitest_<module>) per module, sequential;
82
+ PASS results are recorded in tested.toml; Markdown/JSON report + rich summary
83
+ table. Exit 3 if any module fails.
84
+
85
+ odoo-installer config show | set <key> <value> | edit | path
86
+ odoo-installer version
87
+ ```
88
+
89
+ Manual, step-by-step usage is a first-class goal: every composite action
90
+ (`install`, `instance create`, `module add`) is decomposable into the individual commands
91
+ above, and every step is idempotent so users can drive installation manually.
92
+
93
+ ---
94
+
95
+ ## 3. Architecture
96
+
97
+ ### 3.1 Layering
98
+
99
+ ```text
100
+ ┌─────────────────────────────────────────────────────────┐
101
+ │ cli/ Typer commands: parse → core → render (rich) │ thin, no logic
102
+ ├─────────────────────────────────────────────────────────┤
103
+ │ core/ Business logic, pure Python, no direct I/O │ fully unit-testable
104
+ │ prereqs, stack, instances, modules, runner, tester, │
105
+ │ plan, dbms │
106
+ ├─────────────────────────────────────────────────────────┤
107
+ │ adapters/ The ONLY code that touches the world │ behind Protocols
108
+ │ docker, git, github, system, filesystem │
109
+ ├─────────────────────────────────────────────────────────┤
110
+ │ schemas.py pydantic models (GlobalConfig, Registry, │
111
+ │ InstanceManifest, RepoRecord, TestedModule, ...) │
112
+ │ config.py config resolution + persistence │
113
+ │ console.py rich output helpers, plan/dry-run rendering │
114
+ │ exceptions.py typed error hierarchy │
115
+ └─────────────────────────────────────────────────────────┘
116
+ ```
117
+
118
+ Rules:
119
+
120
+ 1. **cli/ commands never import adapters.** Commands build inputs, call `core`, render
121
+ results. The single exception is the composition root `cli/deps.py`, which wires the
122
+ real adapters into a `Container` (rule 2).
123
+ 2. **core/ depends on adapters only through `typing.Protocol` interfaces** (e.g.
124
+ `DockerLike`, `GitLike`, `SystemLike`), injected as constructor arguments. Tests pass
125
+ fakes; production wires real adapters in `cli/`.
126
+ 3. **All external effects go through adapters** — no `subprocess`, `httpx`, or raw file
127
+ writes outside `adapters/` and `config.py`.
128
+ 4. Every mutating core function returns a **plan object** (list of concrete steps) that the
129
+ CLI either renders (dry-run) or executes (`--apply`). This makes dry-run exact by
130
+ construction — the printed plan *is* the executed code path.
131
+
132
+ ### 3.2 Package layout
133
+
134
+ ```text
135
+ src/odoo_installer/
136
+ ├── __init__.py # __version__
137
+ ├── __main__.py # python -m odoo_installer
138
+ ├── constants.py # ODOO_VERSION="19.0", DEFAULT_IMAGE="odoo:19.0", ports, names
139
+ ├── exceptions.py
140
+ ├── schemas.py # GlobalConfig, Registry(Entry), InstanceManifest, RepoRecord, TestedModule/Registry
141
+ ├── config.py # TOML load/merge/save, path resolution (platformdirs)
142
+ ├── console.py # rich console, tables, plan renderer, --json output
143
+ ├── cli/
144
+ │ ├── main.py # Typer app assembly, global callbacks, version, completion
145
+ │ ├── deps.py # composition root: builds Container with real adapters (rule 2)
146
+ │ ├── common.py # shared command helpers (instance resolution, tested-pass recording)
147
+ │ ├── doctor.py, install.py, instance.py, module.py, db.py, test.py, config.py
148
+ ├── core/
149
+ │ ├── prereqs.py # host prerequisite checks + install plans (pacman/apt)
150
+ │ ├── stack.py # compose/odoo.conf/.env rendering, health wait, addons_path rewrite
151
+ │ ├── instances.py # registry + per-instance manifest CRUD
152
+ │ ├── modules.py # OCA repo resolution, cloning, module discovery
153
+ │ ├── runner.py # odoo command execution inside the web container
154
+ │ ├── tester.py # scratch-DB test runs, log parsing, report building
155
+ │ ├── plan.py # Step model + apply_steps executor (dry-run/--apply)
156
+ │ └── dbms.py # database list/create/drop/reset via psql
157
+ ├── adapters/
158
+ │ ├── docker.py # `docker` / `docker compose` subprocess wrapper
159
+ │ ├── git.py # clone/fetch/checkout/sparse-checkout/rev-parse
160
+ │ ├── github.py # httpx: repo search, branch existence, rate-limit handling
161
+ │ ├── system.py # distro detect (Arch/Debian), pacman/apt command building
162
+ │ └── filesystem.py # paths, atomic file writes, dir scaffolding
163
+ ├── templates/ # docker-compose.yml.j2, odoo.conf.j2, .env.j2 (jinja2)
164
+ └── py.typed
165
+ tests/
166
+ └── unit/ # fakes only, offline, < 5 s
167
+ └── fakes.py # FakeDocker, FakeGit, FakeGitHub, FakeSystem, FakeFs
168
+
169
+ `tests/integration/` (real git/docker, marker `integration`) is deferred to v1.1 — see §8.
170
+ ```
171
+
172
+ ### 3.3 Dependencies
173
+
174
+ Runtime: `typer`, `rich`, `pydantic>=2`, `httpx`, `jinja2`, `tomli-w` (reads use stdlib
175
+ `tomllib`), `platformdirs`.
176
+ Dev: `pytest`, `pytest-cov`, `pytest-mock`, `ruff`, `mypy`, `pre-commit`, `build`, `twine`.
177
+
178
+ ---
179
+
180
+ ## 4. The generated Docker stack
181
+
182
+ Default instances root: `~/odoo-instances/`. One directory per instance:
183
+
184
+ ```text
185
+ ~/odoo-instances/<name>/
186
+ ├── docker-compose.yml # rendered from templates/
187
+ ├── .env # COMPOSE_PROJECT_NAME, ODOO_IMAGE, PG_TAG, HTTP_PORT, POSTGRES_PASSWORD, ADMIN_PASSWD
188
+ ├── config/odoo.conf # addons_path rewritten by `module add`
189
+ ├── addons/local/ # user's own modules (mounted as /mnt/extra-addons)
190
+ ├── repos/<oca-repo>/ # OCA clones (each mounted as /mnt/oca/<repo>)
191
+ ├── logs/ # captured test/install logs (test-<module>-<ts>.log)
192
+ └── .odoo-installer.json # instance manifest (see §5)
193
+ ```
194
+
195
+ Reference compose shape (what the template must render):
196
+
197
+ ```yaml
198
+ services:
199
+ web:
200
+ image: odoo:19.0 # .env: ODOO_IMAGE
201
+ restart: unless-stopped
202
+ depends_on: { db: { condition: service_healthy } }
203
+ ports: ["8069:8069"] # .env: HTTP_PORT
204
+ environment:
205
+ DB_HOST: db
206
+ DB_PORT: "5432"
207
+ DB_USER: odoo
208
+ DB_PASSWORD: ${POSTGRES_PASSWORD}
209
+ volumes:
210
+ - ./config:/etc/odoo
211
+ - ./addons/local:/mnt/extra-addons
212
+ # module add appends one line per OCA repo: ./repos/<repo>:/mnt/oca/<repo>
213
+ command: ["odoo", "-c", "/etc/odoo/odoo.conf"]
214
+ healthcheck:
215
+ test: ["CMD-SHELL", "curl -f http://localhost:8069/web/health || exit 1"]
216
+ interval: 10s
217
+ timeout: 5s
218
+ retries: 30
219
+ start_period: 30s
220
+ db:
221
+ image: postgres:17 # .env: PG_TAG (configurable)
222
+ restart: unless-stopped
223
+ environment:
224
+ POSTGRES_DB: postgres
225
+ POSTGRES_USER: odoo
226
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
227
+ volumes: [pgdata:/var/lib/postgresql/data]
228
+ healthcheck:
229
+ test: ["CMD-SHELL", "pg_isready -U odoo"]
230
+ interval: 5s
231
+ timeout: 5s
232
+ retries: 30
233
+ volumes: { pgdata: }
234
+ ```
235
+
236
+ - The db service is **not** published on the host; all DB access goes through the stack.
237
+ - `addons_path` in `odoo.conf` starts as `/mnt/extra-addons` and gains `/mnt/oca/<repo>`
238
+ entries when repos are added; `module add` rewrites the file and restarts `web`.
239
+ - Port allocation: first free port in 8069–8099 unless `--http-port` is given (the live
240
+ stack on this machine already owns 8069).
241
+
242
+ ---
243
+
244
+ ## 5. State model
245
+
246
+ | File | Scope | Content |
247
+ |------|-------|---------|
248
+ | `~/.config/odoo-installer/config.toml` | global user config | instance root, default ports, default pg tag, GitHub token env var name |
249
+ | `~/.config/odoo-installer/registry.toml` | instance registry | `name → {dir, http_port, created_at, adopted: bool}` |
250
+ | `~/.config/odoo-installer/tested.toml` | installable-addons whitelist | `module → {repo, branch, commit, db, log_path}`; written by `module test` / `test suite` PASSes; `module install`/`upgrade` refuse modules that are not listed unless `--allow-untested` |
251
+ | `<stack>/.odoo-installer.json` | per instance | schema_version, odoo_version, image, pg_tag, applied steps, added repos `{repo, url, branch, commit, modules, mount}`, adopted flag |
252
+ | `<stack>/repos/<repo>/` | OCA clones | git state is the truth for branch/commit; manifest records the last synced commit |
253
+
254
+ Config precedence: **CLI flags > instance manifest > global config.toml > constants.**
255
+ All config writes are atomic (write temp file + rename).
256
+
257
+ ---
258
+
259
+ ## 6. OCA integration rules
260
+
261
+ The tool must behave exactly like the documented OCA workflow:
262
+
263
+ 1. **Branch rule.** An OCA repo is always checked out at `origin/19.0` — verified to exist
264
+ via the GitHub API *before* cloning. The tool never guesses or falls back to `master`/`main`.
265
+ A missing `19.0` branch is a hard error naming the repo.
266
+ 2. **Remotes.** Clones get `origin = https://github.com/OCA/<repo>.git`. If the user
267
+ supplies `--fork <user>`, `origin` is the fork and `upstream` is OCA (mirrors the
268
+ manual workflow).
269
+ 3. **Sparse mode.** `--sparse` uses `git sparse-checkout` limited to the requested modules
270
+ (plus their manifest dirs) to keep clones small; default is a full clone.
271
+ 4. **Existing checkouts.** `--repo <path>` mounts an existing local checkout instead of
272
+ cloning — this is how the tool coexists with the `~/dev/<repo>` + `~/dev/<repo>-deploy`
273
+ worktree pattern used on this machine: the CLI mounts whatever path it is told to and
274
+ never switches branches in a checkout it does not own.
275
+ 5. **Module discovery.** A module = a directory containing `__manifest__.py`. Discovery
276
+ scans mounted repos; `module list` merges filesystem state with
277
+ `ir_module_module` state (via psql) and reports install state per DB.
278
+ 6. **Install/upgrade semantics.** Runs inside the `web` container as
279
+ `odoo -d <db> -i/-u <module> --stop-after-init --http-port=8071` (alternate port
280
+ convention avoids clashing with the serving process — same as manual practice).
281
+ 7. **Adopted stacks are read-mostly.** `instance adopt` never rewrites compose files;
282
+ it may only append addons mounts if explicitly confirmed, otherwise reports what the
283
+ user must add by hand.
284
+
285
+ ---
286
+
287
+ ## 7. Safety, idempotency, errors
288
+
289
+ - **Plan-first:** `install`, `instance create/remove`, `module add/remove`,
290
+ `db drop/reset`, and any host package operation print a numbered plan of the exact
291
+ commands and file writes, then require `--apply` (or `--yes` for prompts) to execute.
292
+ - **Idempotency:** every step checks current state first (package installed? repo already
293
+ cloned at right commit? addons_path already contains the entry?) and reports
294
+ `already satisfied` instead of redoing work. Re-running a completed install is a no-op.
295
+ - **Exit codes:** `0` success · `1` runtime error · `2` usage error (Typer default) ·
296
+ `3` test failures (test suite) · `4` doctor critical check failed.
297
+ - **Error hierarchy** (`exceptions.py`): `OdooInstallerError` base → `PrerequisiteError`,
298
+ `StackError`, `GitError`, `GitHubError`, `ConfigError`, `ModuleError`, `TestFailureError`.
299
+ The CLI renders user-facing messages and maps errors to the exit codes above
300
+ (no `--debug` flag exists in v1.0).
301
+ - **Live instance care:** commands that could touch the production `odoo` DB on this
302
+ machine require an explicit `--db` value (never a default) when the stack is adopted.
303
+ Scratch DBs used by `test` are named `oitest_*` and dropped afterwards unless `--keep-db`.
304
+
305
+ ---
306
+
307
+ ## 8. Testing strategy
308
+
309
+ Pyramid, enforced by CI:
310
+
311
+ | Level | Scope | Rules |
312
+ |-------|-------|-------|
313
+ | Unit (`tests/unit/`) | core + cli against `FakeDocker`, `FakeGit`, `FakeGitHub`, `FakeSystem`, `tmp_path` | offline, deterministic, < 5 s, no markers; every core function's plan generation AND execution paths covered |
314
+ | Integration (`tests/integration/`, marker `integration`, opt-in via `OII_INTEGRATION=1`) — **deferred to v1.1** | real `git clone` of a small OCA repo, real `docker compose up` on an ephemeral port, full `instance create → module add → module install → test module` cycle on a throwaway stack | planned: run locally and in a CI docker job; tear down everything in `finally` |
315
+ | Live smoke (manual, documented) | adopted `~/Projects/odoo-docker` stack | read-mostly commands + one scratch-DB module test; never mutates the `odoo` DB |
316
+
317
+ **Deferral note (recorded at v0.1.0):** the integration layer and its CI docker job
318
+ were deferred to v1.1 — the M4/M5 acceptance relied on the unit layer plus the
319
+ live-stack smoke instead. The `integration` marker remains registered in
320
+ `pyproject.toml`, but until the layer lands `pytest -m integration` selects zero tests
321
+ and exits 5, so it is **not** part of the v0.1.0 quality gates.
322
+
323
+ Quality gates (every milestone, all green before merge):
324
+
325
+ ```bash
326
+ ruff format --check . && ruff check .
327
+ mypy src
328
+ pytest # unit
329
+ # v1.1: pytest -m integration # when docker/git available
330
+ pytest --cov=src/odoo_installer --cov-report=term-missing # keep ≥ 85% overall
331
+ ```
332
+
333
+ Log-parsing tests use inline recorded-style fixture logs (shaped from real 19.0 runs)
334
+ covering: pass, test failure, import error, missing manifest, "not installable",
335
+ addons-path warning.
336
+
337
+ CI (GitHub Actions): job `lint` (name `lint & types`; ruff format/check, mypy) and job
338
+ `unit` on Python 3.11/3.12/3.13. The docker-based `integration` job lands together with
339
+ the v1.1 integration layer; the workflow file documents this.
340
+
341
+ ---
342
+
343
+ ## 9. Packaging & release
344
+
345
+ ```toml
346
+ [project]
347
+ name = "odoo-installer"
348
+ requires-python = ">=3.11"
349
+ dependencies = [ ... ] # see §3.3
350
+ [project.scripts]
351
+ odoo-installer = "odoo_installer.cli.main:app"
352
+ oii = "odoo_installer.cli.main:app"
353
+ [build-system]
354
+ requires = ["hatchling"]
355
+ build-backend = "hatchling.build"
356
+ ```
357
+
358
+ - Developed with `pip install -e ".[dev]"` in `.venv`; used exactly like that on this machine.
359
+ - Versioning: SemVer, single source in `src/odoo_installer/__init__.py`
360
+ (`hatchling` reads it via `[tool.hatch.version]`).
361
+ - Release: `python -m build`, `twine check dist/*`, changelog in `CHANGELOG.md`
362
+ (Keep a Changelog). v1 target release: `0.1.0` at M6.
363
+
364
+ ---
365
+
366
+ ## 10. Milestones
367
+
368
+ Each milestone ends with all quality gates green and a demo of the listed "done" behavior.
369
+ One logical change per commit; Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`,
370
+ `test:`) on branches `feat/<slug>` — this repo is not an OCA repo, so OCA commit prefixes
371
+ do not apply here.
372
+
373
+ ### M0 — Scaffold
374
+ pyproject, src layout, `.venv`, Typer app with `--version`, ruff/mypy/pytest configured,
375
+ pre-commit, CI workflow, `python -m odoo_installer` works.
376
+ **Done when:** `pip install -e ".[dev]" && odoo-installer --version` succeeds; CI green.
377
+
378
+ ### M1 — Config + doctor
379
+ `config.py` (TOML load/merge/save, precedence), `constants.py`, registry read/write,
380
+ `doctor` with all host checks, rich table + `--json`, exit code 4 semantics.
381
+ **Done when:** doctor reflects this machine's real state (docker present, 8069 busy);
382
+ unit tests cover config precedence and every check's pass/fail branch.
383
+
384
+ ### M2 — Host prereqs + instance lifecycle
385
+ `install` (pacman adapter, plan-first, `--apply`), `instance create/list/show/start/stop/
386
+ restart/remove`, template rendering (compose/.env/odoo.conf), health wait, registry +
387
+ manifest writes, port auto-allocation.
388
+ **Done when:** a real instance `oitest` is created on this machine (dry-run → apply),
389
+ answers `/web/health`, `remove` leaves nothing behind; re-running `create` is a no-op.
390
+
391
+ ### M3 — Adoption + databases
392
+ `instance adopt` (detect web/db services, ports, existing addons mounts), `db
393
+ list/create/drop/reset` via psql exec.
394
+ **Done when:** `~/Projects/odoo-docker` is adopted, `db list` matches `psql -l`, `db drop`
395
+ refuses without `--yes`.
396
+
397
+ ### M4 — OCA modules
398
+ `module add` (branch verification, clone/sparse/existing-checkout, mount + addons_path
399
+ rewrite + restart), `module list/search/install/upgrade/remove`, GitHub search adapter.
400
+ **Done when:** an OCA module is added from GitHub at `origin/19.0`, installed into a
401
+ scratch DB inside the adopted stack, and listed with correct install state; integration
402
+ test runs the same flow against a throwaway stack.
403
+
404
+ ### M5 — Test suite
405
+ `test module` (scratch DB, `--test-tags /<module>`, log capture to `logs/`, parser),
406
+ `test suite` with per-module scratch DBs, md/json reports, rich summary, exit code 3.
407
+ **Done when:** `test suite` on the adopted stack produces a correct PASS/FAIL report for
408
+ every module on the addons_path; fixture-log unit tests prove each failure class parses.
409
+
410
+ ### M6 — Polish & release ✔ (v0.1.0, 2026-08-31)
411
+ README, `--help` UX pass, shell completion (`--install-completion`), error message audit,
412
+ coverage ≥ 85%, `python -m build` + `twine check`, CHANGELOG, tag `0.1.0`, optional
413
+ TestPyPI publish.
414
+ **Done:** coverage 90.7% (filesystem adapter unit-tested; subprocess adapters omitted with
415
+ rationale in `pyproject.toml`), wheel + sdist pass `twine check` and a clean-venv smoke
416
+ test, README/CHANGELOG finalized, `v0.1.0` tagged. TestPyPI publish left as an optional
417
+ follow-up; published to PyPI as 0.1.1 (2026-09-01).
418
+
419
+ **Deferral note:** the "integration test runs the same flow against a throwaway stack"
420
+ clauses in the M4/M5 acceptance criteria were satisfied via the unit layer + live-stack
421
+ smoke instead; the throwaway-stack integration test layer is deferred to v1.1 (see §8).
422
+
423
+ ### v1.1 roadmap (priority order)
424
+
425
+ 1. **Integration test layer** (`tests/integration/`, marker `integration`) and the CI
426
+ docker job — the biggest open gap: automated end-to-end
427
+ `instance create → module add → module install → test module` on a throwaway stack,
428
+ run locally and in CI.
429
+ 2. `module upgrade-repos` — re-sync OCA clones to their recorded 19.0 branches.
430
+ 3. apt adapter validation on a real Debian/Ubuntu machine (D3's "later milestone").
431
+ 4. Publish to PyPI — ✔ done (0.1.1, 2026-09-01); README switched back to
432
+ `pip install odoo-installer`.
433
+ 5. Backlog from §1 non-goals: DB backup/restore, SMTP setup wizard, reverse-proxy/TLS
434
+ generation.
435
+
436
+ ---
437
+
438
+ ## 11. Local development setup
439
+
440
+ ```bash
441
+ cd /home/volkan/Projects/dev/odoo-installer
442
+ python -m venv .venv
443
+ source .venv/bin/activate
444
+ pip install -U pip
445
+ pip install -e ".[dev]"
446
+ pre-commit install
447
+ pytest # unit suite
448
+ odoo-installer --version
449
+ ```
450
+
451
+ Notes for this machine: system Python is 3.14 (Arch); if a pinned dev dependency lags on
452
+ 3.14, create the venv with an older interpreter rather than dropping the floor below 3.11.
453
+ The live Odoo 19.0 stack runs at `~/Projects/odoo-docker` (ports 8069) — port auto-allocation
454
+ and the adopted-stack care rules in §7 exist because of it.
455
+
456
+ ## 12. Risks & mitigations
457
+
458
+ | Risk | Mitigation |
459
+ |------|------------|
460
+ | Python 3.14 + pinned deps lag | floor stays 3.11; pin dev deps; venv may use an older interpreter |
461
+ | Odoo weekly image tags drift | default `odoo:19.0`; `--image` override recorded in the manifest |
462
+ | OCA branch moves fast (ocabot bumps) | manifest stores last synced commit; `module upgrade-repos` (deferred to v1.1) re-syncs |
463
+ | Log parsing fragility | parser matched against recorded fixture logs from real runs; exit code is primary signal |
464
+ | Destructive ops on the live stack | plan-first + `--yes`, explicit `--db`, adopted stacks read-mostly, scratch DB naming `oitest_*` |
465
+ | GitHub rate limits | `GITHUB_TOKEN`/`GH_TOKEN` env support; graceful degradation to offline discovery |
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Volkan TASCI
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.