odoorun 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.
- odoorun-0.1.1/.github/workflows/release.yml +34 -0
- odoorun-0.1.1/.gitignore +10 -0
- odoorun-0.1.1/.python-version +1 -0
- odoorun-0.1.1/LICENSE +21 -0
- odoorun-0.1.1/PKG-INFO +152 -0
- odoorun-0.1.1/README.md +129 -0
- odoorun-0.1.1/pyproject.toml +41 -0
- odoorun-0.1.1/src/odoorun/__init__.py +8 -0
- odoorun-0.1.1/src/odoorun/__main__.py +81 -0
- odoorun-0.1.1/src/odoorun/cli.py +88 -0
- odoorun-0.1.1/src/odoorun/completion/__init__.py +0 -0
- odoorun-0.1.1/src/odoorun/completion/database.py +37 -0
- odoorun-0.1.1/src/odoorun/completion/engine.py +15 -0
- odoorun-0.1.1/src/odoorun/discovery.py +89 -0
- odoorun-0.1.1/src/odoorun/runner.py +88 -0
- odoorun-0.1.1/tests/test_cli.py +39 -0
- odoorun-0.1.1/tests/test_completion.py +34 -0
- odoorun-0.1.1/tests/test_discovery.py +94 -0
- odoorun-0.1.1/tests/test_runner.py +60 -0
- odoorun-0.1.1/uv.lock +103 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
name: Build and publish package
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Check out repository
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.x"
|
|
27
|
+
|
|
28
|
+
- name: Build distributions
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade build
|
|
31
|
+
python -m build
|
|
32
|
+
|
|
33
|
+
- name: Publish to PyPI
|
|
34
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
odoorun-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.10
|
odoorun-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Khalid A. Dev
|
|
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.
|
odoorun-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: odoorun
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A modern CLI for running and managing Odoo projects.
|
|
5
|
+
Project-URL: Homepage, https://github.com/khalid99io/odoorun
|
|
6
|
+
Project-URL: Repository, https://github.com/khalid99io/odoorun
|
|
7
|
+
Project-URL: Issues, https://github.com/khalid99io/odoorun/issues
|
|
8
|
+
Author-email: "Khalid A. Dev" <khalid.a.dev@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,odoo
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: rich>=15.0.0
|
|
21
|
+
Requires-Dist: typer>=0.26.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# odoorun
|
|
25
|
+
|
|
26
|
+
`odoorun` is a portable command-line launcher for Odoo. It finds an Odoo
|
|
27
|
+
executable from the current project (or a parent directory), prepares the
|
|
28
|
+
appropriate addons path, and forwards the remaining arguments unchanged.
|
|
29
|
+
|
|
30
|
+
It does not require symlinks, shell aliases, a modified `.bashrc`, or a
|
|
31
|
+
machine-specific configuration.
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Python 3.10 or newer
|
|
36
|
+
- An Odoo installation or source checkout
|
|
37
|
+
- `psql` is optional and is used only for database completion/diagnostics
|
|
38
|
+
|
|
39
|
+
Odoo and PostgreSQL are external dependencies; `odoorun` does not install or
|
|
40
|
+
configure them.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### Recommended: install from PyPI
|
|
45
|
+
|
|
46
|
+
Once published, install it on any supported machine with:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv tool install odoorun
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or use pip:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pip install odoorun
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Install directly from GitHub
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv tool install git+https://github.com/khalid99io/odoorun.git
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Install a local checkout (development)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv tool install .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
After changing the checkout, refresh the installed command:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv tool install --force .
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
Run from an Odoo source checkout or any directory below it:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
odoorun -d my_database --dev=all
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The short command `o` is optional and is not installed by this package. If
|
|
85
|
+
desired, add your own alias or shell function:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
alias o=odoorun
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Virtual-environment projects
|
|
92
|
+
|
|
93
|
+
For a project directory named `my-project`, `odoorun` looks for:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
~/venvs/my-project/bin/odoo
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
When found, it adds `--addons=odoo/addons` automatically. If the virtual
|
|
100
|
+
environment has a different name, configure it without editing the package:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
export ODOORUN_VENV_NAME=my-project-venv
|
|
104
|
+
export ODOORUN_VENV_ROOT="$HOME/venvs"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The tool invokes the venv executable directly; sourcing `activate` is not
|
|
108
|
+
required for the launched Odoo process. A subprocess cannot change the parent
|
|
109
|
+
shell's prompt, so prompt decoration remains a shell responsibility.
|
|
110
|
+
|
|
111
|
+
### Odoo source checkouts
|
|
112
|
+
|
|
113
|
+
For a checkout containing an executable `odoo-bin`, the built-in `addons`
|
|
114
|
+
directory is passed automatically. Additional addon directories can be listed
|
|
115
|
+
with `-a` or `--addons-path`; relative paths are resolved from the checkout's
|
|
116
|
+
parent directory:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
odoorun -a custom-addons,enterprise -d my_database
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Each directory must exist. Missing directories are reported before Odoo starts.
|
|
123
|
+
|
|
124
|
+
## Diagnostics
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
odoorun doctor
|
|
128
|
+
odoorun --help
|
|
129
|
+
odoorun --version
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
uv run python -m unittest discover -s tests -v
|
|
136
|
+
uv run python -m compileall -q src tests
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Publishing
|
|
140
|
+
|
|
141
|
+
The source repository is hosted on GitHub:
|
|
142
|
+
|
|
143
|
+
https://github.com/khalid99io/odoorun
|
|
144
|
+
|
|
145
|
+
To publish releases on PyPI, create a PyPI account, configure a trusted
|
|
146
|
+
publisher for this GitHub repository (recommended), build the package, and
|
|
147
|
+
upload it with `twine` or a GitHub Actions release workflow. A GitHub account
|
|
148
|
+
is required for the repository; a separate PyPI account is required to publish
|
|
149
|
+
the `odoorun` package name.
|
|
150
|
+
|
|
151
|
+
Before the first release, update the version in `pyproject.toml`, add release
|
|
152
|
+
notes, and verify the package in a clean virtual environment.
|
odoorun-0.1.1/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# odoorun
|
|
2
|
+
|
|
3
|
+
`odoorun` is a portable command-line launcher for Odoo. It finds an Odoo
|
|
4
|
+
executable from the current project (or a parent directory), prepares the
|
|
5
|
+
appropriate addons path, and forwards the remaining arguments unchanged.
|
|
6
|
+
|
|
7
|
+
It does not require symlinks, shell aliases, a modified `.bashrc`, or a
|
|
8
|
+
machine-specific configuration.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Python 3.10 or newer
|
|
13
|
+
- An Odoo installation or source checkout
|
|
14
|
+
- `psql` is optional and is used only for database completion/diagnostics
|
|
15
|
+
|
|
16
|
+
Odoo and PostgreSQL are external dependencies; `odoorun` does not install or
|
|
17
|
+
configure them.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
### Recommended: install from PyPI
|
|
22
|
+
|
|
23
|
+
Once published, install it on any supported machine with:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv tool install odoorun
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or use pip:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m pip install odoorun
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Install directly from GitHub
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv tool install git+https://github.com/khalid99io/odoorun.git
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Install a local checkout (development)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
After changing the checkout, refresh the installed command:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv tool install --force .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
Run from an Odoo source checkout or any directory below it:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
odoorun -d my_database --dev=all
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The short command `o` is optional and is not installed by this package. If
|
|
62
|
+
desired, add your own alias or shell function:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
alias o=odoorun
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Virtual-environment projects
|
|
69
|
+
|
|
70
|
+
For a project directory named `my-project`, `odoorun` looks for:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
~/venvs/my-project/bin/odoo
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
When found, it adds `--addons=odoo/addons` automatically. If the virtual
|
|
77
|
+
environment has a different name, configure it without editing the package:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export ODOORUN_VENV_NAME=my-project-venv
|
|
81
|
+
export ODOORUN_VENV_ROOT="$HOME/venvs"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The tool invokes the venv executable directly; sourcing `activate` is not
|
|
85
|
+
required for the launched Odoo process. A subprocess cannot change the parent
|
|
86
|
+
shell's prompt, so prompt decoration remains a shell responsibility.
|
|
87
|
+
|
|
88
|
+
### Odoo source checkouts
|
|
89
|
+
|
|
90
|
+
For a checkout containing an executable `odoo-bin`, the built-in `addons`
|
|
91
|
+
directory is passed automatically. Additional addon directories can be listed
|
|
92
|
+
with `-a` or `--addons-path`; relative paths are resolved from the checkout's
|
|
93
|
+
parent directory:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
odoorun -a custom-addons,enterprise -d my_database
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Each directory must exist. Missing directories are reported before Odoo starts.
|
|
100
|
+
|
|
101
|
+
## Diagnostics
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
odoorun doctor
|
|
105
|
+
odoorun --help
|
|
106
|
+
odoorun --version
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv run python -m unittest discover -s tests -v
|
|
113
|
+
uv run python -m compileall -q src tests
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Publishing
|
|
117
|
+
|
|
118
|
+
The source repository is hosted on GitHub:
|
|
119
|
+
|
|
120
|
+
https://github.com/khalid99io/odoorun
|
|
121
|
+
|
|
122
|
+
To publish releases on PyPI, create a PyPI account, configure a trusted
|
|
123
|
+
publisher for this GitHub repository (recommended), build the package, and
|
|
124
|
+
upload it with `twine` or a GitHub Actions release workflow. A GitHub account
|
|
125
|
+
is required for the repository; a separate PyPI account is required to publish
|
|
126
|
+
the `odoorun` package name.
|
|
127
|
+
|
|
128
|
+
Before the first release, update the version in `pyproject.toml`, add release
|
|
129
|
+
notes, and verify the package in a clean virtual environment.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "odoorun"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "A modern CLI for running and managing Odoo projects."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "Khalid A. Dev", email = "khalid.a.dev@gmail.com" }]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
keywords = ["cli", "odoo"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"rich>=15.0.0",
|
|
21
|
+
"typer>=0.26.8",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/khalid99io/odoorun"
|
|
26
|
+
Repository = "https://github.com/khalid99io/odoorun"
|
|
27
|
+
Issues = "https://github.com/khalid99io/odoorun/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
odoorun = "odoorun.__main__:main"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 88
|
|
38
|
+
target-version = "py310"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["B", "E", "F", "I", "UP"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.markup import escape
|
|
5
|
+
|
|
6
|
+
from .cli import cli
|
|
7
|
+
from .discovery import OdooExecutableNotFoundError
|
|
8
|
+
from .runner import OdooArgumentError, OdooExecutionError, run_odoo
|
|
9
|
+
|
|
10
|
+
ODOORUN_ARGUMENTS = {
|
|
11
|
+
"--help",
|
|
12
|
+
"--version",
|
|
13
|
+
"-h",
|
|
14
|
+
"doctor",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
error_console = Console(stderr=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def show_odoo_not_found(error: OdooExecutableNotFoundError) -> None:
|
|
21
|
+
error_console.print()
|
|
22
|
+
error_console.print("[bold red]Odoo executable not found[/bold red]")
|
|
23
|
+
error_console.print(
|
|
24
|
+
"odoorun looked for [bold]odoo-bin[/bold] in "
|
|
25
|
+
f"[cyan]{escape(str(error.directory))}[/cyan] or its parents, and "
|
|
26
|
+
"[bold]odoo[/bold] on your PATH."
|
|
27
|
+
)
|
|
28
|
+
error_console.print()
|
|
29
|
+
error_console.print("[bold]Try one of these:[/bold]")
|
|
30
|
+
|
|
31
|
+
if error.non_executable is not None:
|
|
32
|
+
candidate = escape(str(error.non_executable))
|
|
33
|
+
error_console.print(
|
|
34
|
+
f" • Make [cyan]{candidate}[/cyan] executable with "
|
|
35
|
+
f"[bold]chmod +x {candidate}[/bold]."
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
error_console.print(
|
|
39
|
+
" • Run [bold]odoorun[/bold] from inside an Odoo source directory."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
error_console.print(" • Install Odoo or add its executable to your PATH.")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def show_execution_error(error: OdooExecutionError) -> None:
|
|
46
|
+
error_console.print()
|
|
47
|
+
error_console.print("[bold red]Odoo could not be started[/bold red]")
|
|
48
|
+
error_console.print(
|
|
49
|
+
f"[cyan]{escape(error.executable)}[/cyan]: {escape(error.reason)}"
|
|
50
|
+
)
|
|
51
|
+
error_console.print("Check the file permissions and try again.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def show_argument_error(error: OdooArgumentError) -> None:
|
|
55
|
+
error_console.print()
|
|
56
|
+
error_console.print("[bold red]Invalid Odoo addons configuration[/bold red]")
|
|
57
|
+
error_console.print(f"[yellow]{escape(str(error))}[/yellow]")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main() -> None:
|
|
61
|
+
argv = sys.argv[1:]
|
|
62
|
+
|
|
63
|
+
if argv and argv[0] in ODOORUN_ARGUMENTS:
|
|
64
|
+
cli(args=argv, prog_name="odoorun")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
run_odoo(argv)
|
|
69
|
+
except OdooExecutableNotFoundError as error:
|
|
70
|
+
show_odoo_not_found(error)
|
|
71
|
+
raise SystemExit(1) from None
|
|
72
|
+
except OdooExecutionError as error:
|
|
73
|
+
show_execution_error(error)
|
|
74
|
+
raise SystemExit(1) from None
|
|
75
|
+
except OdooArgumentError as error:
|
|
76
|
+
show_argument_error(error)
|
|
77
|
+
raise SystemExit(2) from None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.markup import escape
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .discovery import OdooExecutableNotFoundError, find_odoo_executable
|
|
12
|
+
|
|
13
|
+
cli = typer.Typer(
|
|
14
|
+
add_completion=False,
|
|
15
|
+
help="Run Odoo from any directory inside an Odoo project.",
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
rich_markup_mode="rich",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def show_version(value: bool) -> None:
|
|
22
|
+
if value:
|
|
23
|
+
typer.echo(f"odoorun {__version__}")
|
|
24
|
+
raise typer.Exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cli.callback()
|
|
28
|
+
def root(
|
|
29
|
+
version: Annotated[
|
|
30
|
+
bool,
|
|
31
|
+
typer.Option(
|
|
32
|
+
"--version",
|
|
33
|
+
callback=show_version,
|
|
34
|
+
is_eager=True,
|
|
35
|
+
help="Show the installed version and exit.",
|
|
36
|
+
),
|
|
37
|
+
] = False,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Inspect the environment or pass arguments directly to Odoo."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@cli.command()
|
|
43
|
+
def doctor() -> None:
|
|
44
|
+
"""Check whether odoorun can locate its external dependencies."""
|
|
45
|
+
console = Console()
|
|
46
|
+
current = Path.cwd().resolve()
|
|
47
|
+
psql = shutil.which("psql")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
executable = find_odoo_executable(current)
|
|
51
|
+
odoo_status = f"[green]found[/green] — {escape(executable)}"
|
|
52
|
+
healthy = True
|
|
53
|
+
except OdooExecutableNotFoundError as error:
|
|
54
|
+
if error.non_executable is not None:
|
|
55
|
+
detail = f"not executable — {escape(str(error.non_executable))}"
|
|
56
|
+
else:
|
|
57
|
+
detail = "not found"
|
|
58
|
+
odoo_status = f"[red]{detail}[/red]"
|
|
59
|
+
healthy = False
|
|
60
|
+
|
|
61
|
+
table = Table(title="odoorun doctor", show_header=False, box=None)
|
|
62
|
+
table.add_column(style="bold")
|
|
63
|
+
table.add_column()
|
|
64
|
+
table.add_row("Working directory", escape(str(current)))
|
|
65
|
+
table.add_row("Odoo", odoo_status)
|
|
66
|
+
table.add_row(
|
|
67
|
+
"PostgreSQL client",
|
|
68
|
+
(
|
|
69
|
+
f"[green]found[/green] — {escape(psql)}"
|
|
70
|
+
if psql
|
|
71
|
+
else "[yellow]not found[/yellow]"
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
console.print(table)
|
|
75
|
+
|
|
76
|
+
if not healthy:
|
|
77
|
+
console.print(
|
|
78
|
+
"\n[red]Odoo is not ready.[/red] Run this command inside an Odoo "
|
|
79
|
+
"checkout or add [bold]odoo[/bold] to your PATH."
|
|
80
|
+
)
|
|
81
|
+
raise typer.Exit(1)
|
|
82
|
+
|
|
83
|
+
if psql is None:
|
|
84
|
+
console.print(
|
|
85
|
+
"\n[yellow]Odoo is ready, but database completion requires psql.[/yellow]"
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
console.print("\n[green]Everything looks ready.[/green]")
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
QUERY_TIMEOUT_SECONDS = 2
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def complete(prefix: str) -> list[str]:
|
|
9
|
+
"""Return PostgreSQL databases matching ``prefix``.
|
|
10
|
+
|
|
11
|
+
Completion must stay responsive, so unavailable or slow PostgreSQL
|
|
12
|
+
connections are treated as having no suggestions.
|
|
13
|
+
"""
|
|
14
|
+
command = [
|
|
15
|
+
"psql",
|
|
16
|
+
"-Atqc",
|
|
17
|
+
"SELECT datname FROM pg_database WHERE datistemplate = false;",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
command,
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True,
|
|
25
|
+
check=True,
|
|
26
|
+
timeout=QUERY_TIMEOUT_SECONDS,
|
|
27
|
+
)
|
|
28
|
+
except (subprocess.SubprocessError, OSError, UnicodeError):
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
return sorted(
|
|
32
|
+
{
|
|
33
|
+
database
|
|
34
|
+
for database in result.stdout.splitlines()
|
|
35
|
+
if database and database.startswith(prefix)
|
|
36
|
+
}
|
|
37
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from . import database
|
|
4
|
+
|
|
5
|
+
Completer = Callable[[str], list[str]]
|
|
6
|
+
|
|
7
|
+
COMPLETERS: dict[str, Completer] = {
|
|
8
|
+
"database": database.complete,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def complete(kind: str, prefix: str) -> list[str]:
|
|
13
|
+
"""Return completion candidates for a registered completion kind."""
|
|
14
|
+
completer = COMPLETERS.get(kind)
|
|
15
|
+
return completer(prefix) if completer is not None else []
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OdooExecutableNotFoundError(RuntimeError):
|
|
7
|
+
"""Raised when no runnable Odoo executable can be discovered."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
directory: Path,
|
|
12
|
+
non_executable: Path | None = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
self.directory = directory
|
|
15
|
+
self.non_executable = non_executable
|
|
16
|
+
super().__init__("Could not locate an Odoo executable.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def find_local_odoo_executable(
|
|
20
|
+
start_directory: Path,
|
|
21
|
+
) -> tuple[Path | None, Path | None]:
|
|
22
|
+
"""Find an executable ``odoo-bin`` in a directory or one of its parents.
|
|
23
|
+
|
|
24
|
+
The second item is the nearest non-executable candidate, when one exists.
|
|
25
|
+
"""
|
|
26
|
+
non_executable: Path | None = None
|
|
27
|
+
|
|
28
|
+
for directory in (start_directory, *start_directory.parents):
|
|
29
|
+
candidate = directory / "odoo-bin"
|
|
30
|
+
|
|
31
|
+
if not candidate.is_file():
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
if os.access(candidate, os.X_OK):
|
|
35
|
+
return candidate, non_executable
|
|
36
|
+
|
|
37
|
+
if non_executable is None:
|
|
38
|
+
non_executable = candidate
|
|
39
|
+
|
|
40
|
+
return None, non_executable
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _venv_names(project_name: str) -> tuple[str, ...]:
|
|
44
|
+
"""Return configured and conventional virtual-environment names."""
|
|
45
|
+
configured = os.environ.get("ODOORUN_VENV_NAME", "").strip()
|
|
46
|
+
return tuple(dict.fromkeys(name for name in (configured, project_name) if name))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def find_venv_odoo_executable(start_directory: Path) -> str | None:
|
|
50
|
+
"""Find ``odoo`` in the conventional ``~/venvs/<project>/bin`` location.
|
|
51
|
+
|
|
52
|
+
This is the process-local equivalent of sourcing an activation script:
|
|
53
|
+
invoking the executable from the venv gives Odoo the correct interpreter
|
|
54
|
+
and environment without attempting to modify the caller's shell.
|
|
55
|
+
"""
|
|
56
|
+
venv_root = Path(
|
|
57
|
+
os.environ.get("ODOORUN_VENV_ROOT", str(Path.home() / "venvs"))
|
|
58
|
+
).expanduser()
|
|
59
|
+
for directory in (start_directory, *start_directory.parents):
|
|
60
|
+
for name in _venv_names(directory.name):
|
|
61
|
+
candidate = venv_root / name / "bin" / "odoo"
|
|
62
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
63
|
+
return str(candidate)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_odoo_executable(start_directory: Path | None = None) -> str:
|
|
68
|
+
"""Return the best available Odoo executable.
|
|
69
|
+
|
|
70
|
+
A project-local ``odoo-bin`` takes precedence over an ``odoo`` command on
|
|
71
|
+
``PATH``. Parent directories are searched so the command also works from
|
|
72
|
+
inside an add-on or another nested project directory.
|
|
73
|
+
"""
|
|
74
|
+
current = (start_directory or Path.cwd()).resolve()
|
|
75
|
+
local_executable, non_executable = find_local_odoo_executable(current)
|
|
76
|
+
|
|
77
|
+
if local_executable is not None:
|
|
78
|
+
return str(local_executable)
|
|
79
|
+
|
|
80
|
+
venv_executable = find_venv_odoo_executable(current)
|
|
81
|
+
if venv_executable is not None:
|
|
82
|
+
return venv_executable
|
|
83
|
+
|
|
84
|
+
path_executable = shutil.which("odoo")
|
|
85
|
+
|
|
86
|
+
if path_executable is not None:
|
|
87
|
+
return path_executable
|
|
88
|
+
|
|
89
|
+
raise OdooExecutableNotFoundError(current, non_executable)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import NoReturn
|
|
4
|
+
|
|
5
|
+
from .discovery import find_odoo_executable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OdooExecutionError(RuntimeError):
|
|
9
|
+
"""Raised when a discovered Odoo executable cannot be started."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, executable: str, reason: str) -> None:
|
|
12
|
+
self.executable = executable
|
|
13
|
+
self.reason = reason
|
|
14
|
+
super().__init__(f"Could not start {executable}: {reason}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OdooArgumentError(RuntimeError):
|
|
18
|
+
"""Raised when odoorun cannot construct a valid Odoo command."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_venv_odoo(executable: str) -> bool:
|
|
22
|
+
"""Identify an Odoo executable selected from the configured venv root."""
|
|
23
|
+
if Path(executable).name != "odoo":
|
|
24
|
+
return False
|
|
25
|
+
root = Path(
|
|
26
|
+
os.environ.get("ODOORUN_VENV_ROOT", str(Path.home() / "venvs"))
|
|
27
|
+
).expanduser().resolve()
|
|
28
|
+
try:
|
|
29
|
+
relative = Path(executable).resolve().relative_to(root)
|
|
30
|
+
except ValueError:
|
|
31
|
+
return False
|
|
32
|
+
return len(relative.parts) == 3 and relative.parts[-2:] == ("bin", "odoo")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _custom_addons(args: list[str]) -> tuple[list[str], list[str]]:
|
|
36
|
+
"""Remove odoorun's ``-a`` option and return its comma-separated values."""
|
|
37
|
+
forwarded: list[str] = []
|
|
38
|
+
custom: list[str] = []
|
|
39
|
+
index = 0
|
|
40
|
+
while index < len(args):
|
|
41
|
+
value = args[index]
|
|
42
|
+
if value in {"-a", "--addons-path"}:
|
|
43
|
+
index += 1
|
|
44
|
+
if index == len(args) or not args[index].strip():
|
|
45
|
+
raise OdooArgumentError(f"{value} requires a comma-separated path list")
|
|
46
|
+
custom.extend(part.strip() for part in args[index].split(",") if part.strip())
|
|
47
|
+
elif value.startswith("-a=") or value.startswith("--addons-path="):
|
|
48
|
+
custom.extend(part.strip() for part in value.split("=", 1)[1].split(",") if part.strip())
|
|
49
|
+
else:
|
|
50
|
+
forwarded.append(value)
|
|
51
|
+
index += 1
|
|
52
|
+
return forwarded, custom
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_odoo_args(executable: str, args: list[str]) -> list[str]:
|
|
56
|
+
"""Add context-specific addon paths and validate custom addon folders."""
|
|
57
|
+
forwarded, custom = _custom_addons(args)
|
|
58
|
+
if Path(executable).name != "odoo-bin":
|
|
59
|
+
if not _is_venv_odoo(executable):
|
|
60
|
+
return forwarded
|
|
61
|
+
if not any(arg == "--addons" or arg.startswith("--addons=") for arg in forwarded):
|
|
62
|
+
forwarded.insert(0, "--addons=odoo/addons")
|
|
63
|
+
return forwarded
|
|
64
|
+
|
|
65
|
+
repo_root = Path(executable).parent
|
|
66
|
+
addons = repo_root / "addons"
|
|
67
|
+
if not addons.is_dir():
|
|
68
|
+
raise OdooArgumentError(f"Odoo addons directory not found: {addons}")
|
|
69
|
+
paths = [str(addons)]
|
|
70
|
+
for item in custom:
|
|
71
|
+
candidate = Path(item).expanduser()
|
|
72
|
+
if not candidate.is_absolute():
|
|
73
|
+
candidate = repo_root.parent / candidate
|
|
74
|
+
if not candidate.is_dir():
|
|
75
|
+
raise OdooArgumentError(f"Custom addons directory not found: {candidate}")
|
|
76
|
+
paths.append(str(candidate))
|
|
77
|
+
return ["--addons-path=" + ",".join(paths), *forwarded]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def run_odoo(args: list[str]) -> NoReturn:
|
|
81
|
+
"""Replace the current process with Odoo and forward all arguments."""
|
|
82
|
+
executable = find_odoo_executable()
|
|
83
|
+
command = [executable, *build_odoo_args(executable, args)]
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
os.execv(executable, command)
|
|
87
|
+
except OSError as error:
|
|
88
|
+
raise OdooExecutionError(executable, error.strerror or str(error)) from error
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from odoorun.cli import cli
|
|
8
|
+
from odoorun.discovery import OdooExecutableNotFoundError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CliTests(unittest.TestCase):
|
|
12
|
+
def setUp(self) -> None:
|
|
13
|
+
self.runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
def test_version_option(self) -> None:
|
|
16
|
+
result = self.runner.invoke(cli, ["--version"])
|
|
17
|
+
|
|
18
|
+
self.assertEqual(result.exit_code, 0)
|
|
19
|
+
self.assertRegex(result.stdout, r"^odoorun \d+\.\d+\.\d+\s*$")
|
|
20
|
+
|
|
21
|
+
@patch("odoorun.cli.shutil.which", return_value=None)
|
|
22
|
+
@patch("odoorun.cli.find_odoo_executable")
|
|
23
|
+
def test_doctor_fails_cleanly_when_odoo_is_missing(
|
|
24
|
+
self,
|
|
25
|
+
find_executable,
|
|
26
|
+
_which,
|
|
27
|
+
) -> None:
|
|
28
|
+
find_executable.side_effect = OdooExecutableNotFoundError(
|
|
29
|
+
Path("/tmp/example-project")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
result = self.runner.invoke(cli, ["doctor"])
|
|
33
|
+
|
|
34
|
+
self.assertEqual(result.exit_code, 1)
|
|
35
|
+
self.assertIn("Odoo is not ready", result.stdout)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
unittest.main()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from odoorun.completion import database
|
|
6
|
+
from odoorun.completion.engine import complete
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatabaseCompletionTests(unittest.TestCase):
|
|
10
|
+
@patch("odoorun.completion.database.subprocess.run")
|
|
11
|
+
def test_returns_sorted_unique_prefix_matches(self, run) -> None:
|
|
12
|
+
run.return_value.stdout = "test_z\nproduction\ntest_a\ntest_a\n"
|
|
13
|
+
|
|
14
|
+
result = database.complete("test_")
|
|
15
|
+
|
|
16
|
+
self.assertEqual(result, ["test_a", "test_z"])
|
|
17
|
+
self.assertEqual(
|
|
18
|
+
run.call_args.kwargs["timeout"],
|
|
19
|
+
database.QUERY_TIMEOUT_SECONDS,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@patch(
|
|
23
|
+
"odoorun.completion.database.subprocess.run",
|
|
24
|
+
side_effect=subprocess.TimeoutExpired("psql", 2),
|
|
25
|
+
)
|
|
26
|
+
def test_returns_no_results_when_psql_times_out(self, _run) -> None:
|
|
27
|
+
self.assertEqual(database.complete(""), [])
|
|
28
|
+
|
|
29
|
+
def test_unknown_completion_kind_returns_no_results(self) -> None:
|
|
30
|
+
self.assertEqual(complete("unknown", ""), [])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
unittest.main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from odoorun.discovery import (
|
|
8
|
+
OdooExecutableNotFoundError,
|
|
9
|
+
find_odoo_executable,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FindOdooExecutableTests(unittest.TestCase):
|
|
14
|
+
def make_executable(self, path: Path) -> None:
|
|
15
|
+
path.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
16
|
+
path.chmod(0o755)
|
|
17
|
+
|
|
18
|
+
def test_finds_odoo_bin_in_parent_directory(self) -> None:
|
|
19
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
20
|
+
root = Path(temporary_directory)
|
|
21
|
+
nested = root / "addons" / "custom"
|
|
22
|
+
nested.mkdir(parents=True)
|
|
23
|
+
executable = root / "odoo-bin"
|
|
24
|
+
self.make_executable(executable)
|
|
25
|
+
|
|
26
|
+
with patch.dict(os.environ, {"PATH": ""}):
|
|
27
|
+
result = find_odoo_executable(nested)
|
|
28
|
+
|
|
29
|
+
self.assertEqual(result, str(executable))
|
|
30
|
+
|
|
31
|
+
def test_local_odoo_bin_takes_precedence_over_path(self) -> None:
|
|
32
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
33
|
+
root = Path(temporary_directory)
|
|
34
|
+
local_executable = root / "odoo-bin"
|
|
35
|
+
path_executable = root / "odoo"
|
|
36
|
+
self.make_executable(local_executable)
|
|
37
|
+
self.make_executable(path_executable)
|
|
38
|
+
|
|
39
|
+
with patch.dict(os.environ, {"PATH": str(root)}):
|
|
40
|
+
result = find_odoo_executable(root)
|
|
41
|
+
|
|
42
|
+
self.assertEqual(result, str(local_executable))
|
|
43
|
+
|
|
44
|
+
def test_falls_back_to_odoo_on_path(self) -> None:
|
|
45
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
46
|
+
root = Path(temporary_directory)
|
|
47
|
+
executable = root / "odoo"
|
|
48
|
+
self.make_executable(executable)
|
|
49
|
+
|
|
50
|
+
with patch.dict(os.environ, {"PATH": str(root)}):
|
|
51
|
+
result = find_odoo_executable(root)
|
|
52
|
+
|
|
53
|
+
self.assertEqual(result, str(executable))
|
|
54
|
+
|
|
55
|
+
def test_finds_odoo_from_project_virtual_environment(self) -> None:
|
|
56
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
57
|
+
home = Path(temporary_directory)
|
|
58
|
+
project = home / "PycharmProjects" / "obusiness-2"
|
|
59
|
+
project.mkdir(parents=True)
|
|
60
|
+
executable = home / "venvs" / "obusiness-v2" / "bin" / "odoo"
|
|
61
|
+
executable.parent.mkdir(parents=True)
|
|
62
|
+
self.make_executable(executable)
|
|
63
|
+
|
|
64
|
+
with patch.dict(
|
|
65
|
+
os.environ,
|
|
66
|
+
{
|
|
67
|
+
"PATH": "",
|
|
68
|
+
"HOME": str(home),
|
|
69
|
+
"ODOORUN_VENV_NAME": "obusiness-v2",
|
|
70
|
+
},
|
|
71
|
+
):
|
|
72
|
+
result = find_odoo_executable(project)
|
|
73
|
+
|
|
74
|
+
self.assertEqual(result, str(executable))
|
|
75
|
+
|
|
76
|
+
def test_reports_nearest_non_executable_odoo_bin(self) -> None:
|
|
77
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
78
|
+
root = Path(temporary_directory)
|
|
79
|
+
nested = root / "addons"
|
|
80
|
+
nested.mkdir()
|
|
81
|
+
candidate = root / "odoo-bin"
|
|
82
|
+
candidate.write_text("#!/bin/sh\n", encoding="utf-8")
|
|
83
|
+
candidate.chmod(0o644)
|
|
84
|
+
|
|
85
|
+
with patch.dict(os.environ, {"PATH": ""}):
|
|
86
|
+
with self.assertRaises(OdooExecutableNotFoundError) as context:
|
|
87
|
+
find_odoo_executable(nested)
|
|
88
|
+
|
|
89
|
+
self.assertEqual(context.exception.directory, nested.resolve())
|
|
90
|
+
self.assertEqual(context.exception.non_executable, candidate)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
unittest.main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from odoorun.runner import OdooArgumentError, OdooExecutionError, build_odoo_args, run_odoo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunOdooTests(unittest.TestCase):
|
|
11
|
+
def test_adds_default_addons_for_venv_odoo(self) -> None:
|
|
12
|
+
self.assertEqual(
|
|
13
|
+
build_odoo_args("/home/khalid/venvs/obusiness-v2/bin/odoo", ["-d", "demo"]),
|
|
14
|
+
["--addons=odoo/addons", "-d", "demo"],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def test_builds_and_validates_source_addons_paths(self) -> None:
|
|
18
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
19
|
+
repo = Path(temporary_directory) / "odoo"
|
|
20
|
+
(repo / "addons").mkdir(parents=True)
|
|
21
|
+
(repo.parent / "custom-addons").mkdir()
|
|
22
|
+
result = build_odoo_args(
|
|
23
|
+
str(repo / "odoo-bin"), ["-a", "custom-addons", "-d", "demo"]
|
|
24
|
+
)
|
|
25
|
+
self.assertEqual(result[0], f"--addons-path={repo / 'addons'},{repo.parent / 'custom-addons'}")
|
|
26
|
+
self.assertEqual(result[1:], ["-d", "demo"])
|
|
27
|
+
|
|
28
|
+
def test_rejects_missing_custom_addons_path(self) -> None:
|
|
29
|
+
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
30
|
+
repo = Path(temporary_directory) / "odoo"
|
|
31
|
+
(repo / "addons").mkdir(parents=True)
|
|
32
|
+
with self.assertRaises(OdooArgumentError):
|
|
33
|
+
build_odoo_args(str(repo / "odoo-bin"), ["-a", "missing"])
|
|
34
|
+
|
|
35
|
+
@patch("odoorun.runner.os.execv")
|
|
36
|
+
@patch("odoorun.runner.find_odoo_executable", return_value="/home/khalid/venvs/demo/bin/odoo")
|
|
37
|
+
def test_forwards_arguments_unchanged(self, find_executable, execv) -> None:
|
|
38
|
+
run_odoo(["--database", "example", "--dev=all"])
|
|
39
|
+
|
|
40
|
+
find_executable.assert_called_once_with()
|
|
41
|
+
execv.assert_called_once_with(
|
|
42
|
+
"/home/khalid/venvs/demo/bin/odoo",
|
|
43
|
+
["/home/khalid/venvs/demo/bin/odoo", "--addons=odoo/addons", "--database", "example", "--dev=all"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@patch(
|
|
47
|
+
"odoorun.runner.os.execv",
|
|
48
|
+
side_effect=PermissionError(13, "Permission denied"),
|
|
49
|
+
)
|
|
50
|
+
@patch("odoorun.runner.find_odoo_executable", return_value="/home/khalid/venvs/demo/bin/odoo")
|
|
51
|
+
def test_wraps_operating_system_errors(self, _find_executable, _execv) -> None:
|
|
52
|
+
with self.assertRaises(OdooExecutionError) as context:
|
|
53
|
+
run_odoo([])
|
|
54
|
+
|
|
55
|
+
self.assertEqual(context.exception.executable, "/home/khalid/venvs/demo/bin/odoo")
|
|
56
|
+
self.assertEqual(context.exception.reason, "Permission denied")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
unittest.main()
|
odoorun-0.1.1/uv.lock
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "annotated-doc"
|
|
7
|
+
version = "0.0.4"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "colorama"
|
|
16
|
+
version = "0.4.6"
|
|
17
|
+
source = { registry = "https://pypi.org/simple" }
|
|
18
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
19
|
+
wheels = [
|
|
20
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "markdown-it-py"
|
|
25
|
+
version = "4.2.0"
|
|
26
|
+
source = { registry = "https://pypi.org/simple" }
|
|
27
|
+
dependencies = [
|
|
28
|
+
{ name = "mdurl" },
|
|
29
|
+
]
|
|
30
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
|
31
|
+
wheels = [
|
|
32
|
+
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[[package]]
|
|
36
|
+
name = "mdurl"
|
|
37
|
+
version = "0.1.2"
|
|
38
|
+
source = { registry = "https://pypi.org/simple" }
|
|
39
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
40
|
+
wheels = [
|
|
41
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[[package]]
|
|
45
|
+
name = "odoorun"
|
|
46
|
+
version = "0.1.0"
|
|
47
|
+
source = { editable = "." }
|
|
48
|
+
dependencies = [
|
|
49
|
+
{ name = "rich" },
|
|
50
|
+
{ name = "typer" },
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[package.metadata]
|
|
54
|
+
requires-dist = [
|
|
55
|
+
{ name = "rich", specifier = ">=15.0.0" },
|
|
56
|
+
{ name = "typer", specifier = ">=0.26.8" },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[[package]]
|
|
60
|
+
name = "pygments"
|
|
61
|
+
version = "2.20.0"
|
|
62
|
+
source = { registry = "https://pypi.org/simple" }
|
|
63
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
64
|
+
wheels = [
|
|
65
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[[package]]
|
|
69
|
+
name = "rich"
|
|
70
|
+
version = "15.0.0"
|
|
71
|
+
source = { registry = "https://pypi.org/simple" }
|
|
72
|
+
dependencies = [
|
|
73
|
+
{ name = "markdown-it-py" },
|
|
74
|
+
{ name = "pygments" },
|
|
75
|
+
]
|
|
76
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
77
|
+
wheels = [
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[[package]]
|
|
82
|
+
name = "shellingham"
|
|
83
|
+
version = "1.5.4"
|
|
84
|
+
source = { registry = "https://pypi.org/simple" }
|
|
85
|
+
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
|
86
|
+
wheels = [
|
|
87
|
+
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[[package]]
|
|
91
|
+
name = "typer"
|
|
92
|
+
version = "0.26.8"
|
|
93
|
+
source = { registry = "https://pypi.org/simple" }
|
|
94
|
+
dependencies = [
|
|
95
|
+
{ name = "annotated-doc" },
|
|
96
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
97
|
+
{ name = "rich" },
|
|
98
|
+
{ name = "shellingham" },
|
|
99
|
+
]
|
|
100
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" }
|
|
101
|
+
wheels = [
|
|
102
|
+
{ url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" },
|
|
103
|
+
]
|