gOdoo-cli 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. godoo_cli/__init__.py +2 -0
  2. godoo_cli/__main__.py +5 -0
  3. godoo_cli/cli.py +87 -0
  4. godoo_cli/cli_common.py +183 -0
  5. godoo_cli/commands/__init__.py +9 -0
  6. godoo_cli/commands/backup/__init__.py +1 -0
  7. godoo_cli/commands/backup/cli.py +17 -0
  8. godoo_cli/commands/backup/load.py +110 -0
  9. godoo_cli/commands/backup/pull.py +134 -0
  10. godoo_cli/commands/backup/util.py +35 -0
  11. godoo_cli/commands/bootstrap.py +187 -0
  12. godoo_cli/commands/config.py +43 -0
  13. godoo_cli/commands/db/__init__.py +1 -0
  14. godoo_cli/commands/db/cli.py +18 -0
  15. godoo_cli/commands/db/connection.py +94 -0
  16. godoo_cli/commands/db/passwords.py +56 -0
  17. godoo_cli/commands/db/query.py +41 -0
  18. godoo_cli/commands/launch.py +325 -0
  19. godoo_cli/commands/rpc/__init__.py +8 -0
  20. godoo_cli/commands/rpc/cli.py +34 -0
  21. godoo_cli/commands/rpc/config_parameters.py +48 -0
  22. godoo_cli/commands/rpc/importer.py +70 -0
  23. godoo_cli/commands/rpc/modules.py +144 -0
  24. godoo_cli/commands/rpc/translations.py +129 -0
  25. godoo_cli/commands/shell.py +45 -0
  26. godoo_cli/commands/source_get.py +279 -0
  27. godoo_cli/commands/test.py +126 -0
  28. godoo_cli/git/__init__.py +4 -0
  29. godoo_cli/git/git_odoo.py +89 -0
  30. godoo_cli/git/git_odoo_addons.py +95 -0
  31. godoo_cli/git/git_repo.py +207 -0
  32. godoo_cli/git/git_url.py +152 -0
  33. godoo_cli/git/zip_download.py +49 -0
  34. godoo_cli/helpers/__init__.py +1 -0
  35. godoo_cli/helpers/bootstrap.py +61 -0
  36. godoo_cli/helpers/cli.py +15 -0
  37. godoo_cli/helpers/odoo_files.py +278 -0
  38. godoo_cli/helpers/odoo_manifest.py +112 -0
  39. godoo_cli/helpers/system.py +176 -0
  40. godoo_cli/version.py +1 -0
  41. godoo_cli-0.7.0.dist-info/LICENSE +165 -0
  42. godoo_cli-0.7.0.dist-info/METADATA +197 -0
  43. godoo_cli-0.7.0.dist-info/RECORD +45 -0
  44. godoo_cli-0.7.0.dist-info/WHEEL +4 -0
  45. godoo_cli-0.7.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,278 @@
1
+ """Functions that operate on Odoos Source Code."""
2
+ import logging
3
+ from ast import literal_eval
4
+ from pathlib import Path
5
+ from typing import List, Union
6
+
7
+ from git import Repo
8
+
9
+ from .system import run_cmd
10
+
11
+ LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ def odoo_bin_get_version(odoo_main_repo_path: Path) -> str:
15
+ """Get Odoo Version by calling 'odoo-bin --version'
16
+
17
+ Parameters
18
+ ----------
19
+ odoo_main_repo_path : Path
20
+ Path to odoo-bin folder
21
+
22
+ Returns
23
+ -------
24
+ str
25
+ odoo-bin --version output
26
+ """
27
+ odoo_bin_path = odoo_main_repo_path / "odoo-bin"
28
+ version_out = run_cmd(f"{odoo_bin_path.absolute()} --version", capture_output=True, text=True)
29
+ return version_out.stdout
30
+
31
+
32
+ def get_zip_addon_path(thirdparty_path: Path) -> Path:
33
+ return thirdparty_path / "custom"
34
+
35
+
36
+ def folder_is_odoo_module(folder: Path) -> bool:
37
+ """Wether folder is a valid odoo module.
38
+
39
+ Parameters
40
+ ----------
41
+ folder : Path
42
+ Folder to check
43
+
44
+ Returns
45
+ -------
46
+ bool
47
+ true if folder contains a module
48
+ """
49
+ return folder.is_dir() and any(folder.glob("__manifest__.py"))
50
+
51
+
52
+ def get_odoo_module_paths(search_folders: Union[List[Path], Path]) -> List[Path]:
53
+ """List all Valid odoo module names in one or many Odoo addons folder.
54
+
55
+ Parameters
56
+ ----------
57
+ search_folders : Path
58
+ Folder(s) to search in.
59
+
60
+ Returns
61
+ -------
62
+ List[Path]
63
+ List of Valid Module Folders within search_folder
64
+ """
65
+ if not isinstance(search_folders, List):
66
+ search_folders = [search_folders]
67
+
68
+ module_paths = []
69
+ for folder in search_folders:
70
+ if not folder.exists():
71
+ return
72
+ for folder in folder.iterdir():
73
+ if folder_is_odoo_module(folder):
74
+ module_paths.append(folder)
75
+ return module_paths
76
+
77
+
78
+ def get_changed_modules(
79
+ addon_path: Path,
80
+ diff_branch: str,
81
+ ) -> List[Path]:
82
+ """Get Paths of changed modules since git diff.
83
+
84
+ Parameters
85
+ ----------
86
+ addon_path : Path
87
+ Folder in git repo where to look for changes
88
+ diff_branch : str
89
+ Branch or diffable ref for git
90
+
91
+ Returns
92
+ -------
93
+ List[Path]
94
+ List of Paths where something has changed since git diff
95
+ """
96
+ addon_path = addon_path.absolute()
97
+ repo = Repo(addon_path, search_parent_directories=True)
98
+ git_root = Path(repo.git.rev_parse("--show-toplevel"))
99
+ changed_module_files = []
100
+ for change in repo.git.diff("--name-status", diff_branch).split("\n"):
101
+ path = git_root / change.split("\t")[1]
102
+ if addon_path in path.parents:
103
+ changed_module_files.append(path)
104
+
105
+ changed_module_folders = []
106
+ for f in changed_module_files:
107
+ for pf in f.parents:
108
+ if pf.absolute() == addon_path.absolute():
109
+ break
110
+ if pf.absolute() not in changed_module_folders and folder_is_odoo_module(pf):
111
+ changed_module_folders.append(pf.absolute())
112
+ if changed_module_folders:
113
+ LOGGER.debug(
114
+ "Found Modules changed to branch '%s':\n %s",
115
+ diff_branch,
116
+ "\n".join(["\t" + str(f) for f in changed_module_folders]),
117
+ )
118
+ return changed_module_folders
119
+
120
+
121
+ def get_depends_of_module(
122
+ all_modules: List[Path],
123
+ module_to_check: Path,
124
+ already_done_modules: List[Path] = None,
125
+ ):
126
+ """Recursively Searches sub dependencies for Odoo modules.
127
+
128
+
129
+ Parameters
130
+ ----------
131
+ all_modules : List[Path]
132
+ List of Path objects pointing to odoo modules
133
+ module_to_check : Path
134
+ path with odoo module
135
+ already_done_modules : List[Path], optional
136
+ Only used internally for recursion caching, by default None
137
+
138
+ Returns
139
+ -------
140
+ List[Path]
141
+ Paths to dependency modules
142
+ """
143
+ manifest_path = module_to_check / "__manifest__.py"
144
+
145
+ if not already_done_modules:
146
+ already_done_modules = []
147
+ if module_to_check.absolute() in already_done_modules:
148
+ return []
149
+ already_done_modules.append(module_to_check.absolute())
150
+
151
+ manifest = literal_eval(manifest_path.read_text())
152
+ module_depends = manifest.get("depends", [])
153
+ sub_depends = []
154
+ for dep in module_depends:
155
+ dep_path = [p for p in all_modules if p.stem == dep]
156
+ if dep_path:
157
+ dep_path = dep_path[0]
158
+ if dep_path.absolute() in already_done_modules:
159
+ continue
160
+ sub_depends.append(dep_path.absolute())
161
+ sub_depends += get_depends_of_module(all_modules, dep_path, already_done_modules)
162
+ elif dep != "base":
163
+ LOGGER.warn("Could not find Dependency: '%s' in available modules", dep)
164
+
165
+ return list(set(sub_depends))
166
+
167
+
168
+ def get_addon_paths(
169
+ odoo_main_repo: Path,
170
+ workspace_addon_path: Path,
171
+ thirdparty_addon_path: Path,
172
+ ) -> List[Path]:
173
+ """Get Odoo Addon Paths for odoo.conf.
174
+
175
+ Parameters
176
+ ----------
177
+ odoo_main_repo : Path
178
+ Path to main odoo repo
179
+ workspace_addon_path : Path
180
+ Path to workspace addons
181
+ thirdparty_addon_path : Path
182
+ path to git cloned addon repos
183
+
184
+ Returns
185
+ -------
186
+ List[Path]
187
+ List of valid addon Paths
188
+ """
189
+ odoo_addon_paths = [odoo_main_repo / "addons"]
190
+ if get_odoo_module_paths(workspace_addon_path):
191
+ odoo_addon_paths.append(workspace_addon_path)
192
+ zip_addon_path = get_zip_addon_path(thirdparty_addon_path)
193
+ zip_addon_repos = [f for f in zip_addon_path.iterdir() if f.is_dir() and get_odoo_module_paths(f)]
194
+ odoo_addon_paths += zip_addon_repos
195
+ git_thirdparty_addon_repos = [
196
+ p for p in thirdparty_addon_path.iterdir() if p.is_dir() and not p.resolve() == zip_addon_path.resolve()
197
+ ]
198
+ odoo_addon_paths += git_thirdparty_addon_repos
199
+ return odoo_addon_paths
200
+
201
+
202
+ def _get_odoo_main_path(addon_paths: List[Path]) -> Path:
203
+ """Get Odoo main path by location of odoo-bin from addon folders.
204
+
205
+ Parameters
206
+ ----------
207
+ addon_paths : List[Path]
208
+ List of possible paths
209
+
210
+ Returns
211
+ -------
212
+ Path
213
+ Path with odoo-bin and main addons folder
214
+ """
215
+ for path in addon_paths:
216
+ # The main odoo addon path is odoo/addons.
217
+ # so one up should be the launch script
218
+ bin_path = path.parent / "odoo-bin"
219
+ if bin_path.exists() and bin_path.is_file():
220
+ return path
221
+
222
+
223
+ def _get_python_requirements_of_modules(addon_paths: List[Path], filter_module_names: List[str] = None):
224
+ """Install python requirements mentioned in module manifests
225
+
226
+ Parameters
227
+ ----------
228
+ addon_paths : List[Path]
229
+ Paths to look for addons. (same as odoo-bin)
230
+ filter_module_names : List[str], optional
231
+ Modules to look for manifests, by default all available modules
232
+ """
233
+ available_modules = get_odoo_module_paths(addon_paths)
234
+ available_module_names = [p.stem for p in available_modules]
235
+
236
+ if not filter_module_names:
237
+ filter_module_names = available_module_names
238
+ filter_module_names = [f for f in filter_module_names if f != "base"]
239
+
240
+ LOGGER.debug("Checking python requirements of Modules: %s", sorted(filter_module_names))
241
+ if unavailable_modules := set(filter_module_names).difference(available_module_names):
242
+ LOGGER.warning("Could not find __manifest__ for: %s", unavailable_modules)
243
+ LOGGER.debug("Search Paths:\n%s", "\n".join(map(str, addon_paths)))
244
+
245
+ check_modules = [mp for mp in available_modules if mp.stem in filter_module_names]
246
+
247
+ if odoo_main_path := _get_odoo_main_path(addon_paths):
248
+ # When the Odoo main path is in the supplied path, exclude those modules.
249
+ # Odoos base requirements are set in their requirements.txt
250
+ LOGGER.debug("Py install: Excluding Odoo main Modules in: '%s'", odoo_main_path)
251
+ check_modules = [mp for mp in check_modules if odoo_main_path not in mp.parents]
252
+
253
+ check_modules_dependencies = []
254
+ for module in check_modules:
255
+ check_modules_dependencies += get_depends_of_module(
256
+ available_modules, module, already_done_modules=check_modules_dependencies
257
+ )
258
+ LOGGER.debug(
259
+ "adding children of requested modules to check list:\n%s",
260
+ "\n".join(sorted([p.stem for p in check_modules_dependencies])),
261
+ )
262
+
263
+ check_modules += check_modules_dependencies
264
+
265
+ check_modules = set(check_modules)
266
+
267
+ if not check_modules:
268
+ LOGGER.debug("No Modules provided to check for python Requirements")
269
+ return
270
+ python_depends = []
271
+ for module_path in check_modules:
272
+ LOGGER.debug("Checking for External Depends: %s", module_path)
273
+ manifest_path = module_path / "__manifest__.py"
274
+ manifest = literal_eval(manifest_path.read_text())
275
+ if module_depends := manifest.get("external_dependencies", {}).get("python"):
276
+ python_depends += module_depends
277
+
278
+ return list(set(python_depends))
@@ -0,0 +1,112 @@
1
+ import logging
2
+ import shutil
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ from ruamel.yaml import YAML
7
+
8
+ from ..git.git_url import GitUrl
9
+
10
+ LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ def remove_unused_folders(thirdparty_addon_path: Path, thirdparty_repos, keep_folders: List[Path]):
14
+ """Remove folders that are not included in git_repos anymore
15
+
16
+ Parameters
17
+ ----------
18
+ thirdparty_addon_path : Path
19
+ Folder to check for deletions
20
+ thirdparty_repos : Dict
21
+ Dict of Prefix:[dict[url],..]
22
+ """
23
+ allowed_folders = []
24
+ keep_folders_absolute = [p.absolute() for p in keep_folders]
25
+ for prefix in thirdparty_repos:
26
+ for repo in thirdparty_repos[prefix]:
27
+ repo_url = GitUrl(repo["url"])
28
+ allowed_folders.append(f"{prefix}_{repo_url.name}")
29
+ for folder in thirdparty_addon_path.iterdir():
30
+ if not folder.is_dir() or folder.absolute() in keep_folders_absolute:
31
+ continue
32
+ if folder.stem not in allowed_folders:
33
+ LOGGER.info("Removing unspecified Addon Folder: %s", folder)
34
+ shutil.rmtree(folder)
35
+
36
+
37
+ def update_yml(
38
+ repo_yml,
39
+ generate_yml_compare_comments: bool = False,
40
+ ):
41
+ """Process yaml after thirdparty clone.
42
+
43
+ Parameters
44
+ ----------
45
+ repo_yml : Yaml Dict
46
+ Ruamel Yaml Dict of prefix and list of repos (url,commit,branch)
47
+ generate_yml_compare_comments : bool, optional
48
+ add github compare links as comment to repo yml, by default False
49
+ """
50
+ thirdparty_repos = repo_yml["thirdparty"]
51
+ odoo_default_branch = repo_yml["odoo"].get("branch")
52
+ if not odoo_default_branch:
53
+ LOGGER.error("Odoo Key in manifest missing branch argument.")
54
+ return
55
+
56
+ for prefix in thirdparty_repos:
57
+ for repo in thirdparty_repos[prefix]:
58
+ if generate_yml_compare_comments:
59
+ yaml_add_compare_commit(repo, odoo_default_branch)
60
+ else:
61
+ yaml_remove_compare_commit(repo)
62
+
63
+
64
+ def yaml_add_compare_commit(repo_dict, compare_target: str):
65
+ """Add comment with Compare URL to Repo:
66
+
67
+ Parameters
68
+ ----------
69
+ repo_dict : _type_
70
+ Yaml Dict of url and commit
71
+ compare_target : str
72
+ git ref to compare to
73
+ """
74
+ git_url = GitUrl(repo_dict["url"])
75
+ try:
76
+ compare_url = git_url.get_compare_url(repo_dict["commit"], compare_target)
77
+ repo_dict.yaml_add_eol_comment(compare_url, "commit")
78
+ except Exception as e:
79
+ LOGGER.warn(f"Cannot Generate compare URL for: {git_url.url}")
80
+ LOGGER.debug(e)
81
+
82
+
83
+ def yaml_remove_compare_commit(repo_dict):
84
+ """Remove Comments that have /compare/ in them.
85
+
86
+ Parameters
87
+ ----------
88
+ repo_dict : RuamelYaml Dict
89
+ yaml dict
90
+ """
91
+ del_list = []
92
+ for target, comments in repo_dict.ca.items.items():
93
+ for subcomment in comments:
94
+ if subcomment and "/compare/" in subcomment.value:
95
+ del_list.append(target)
96
+
97
+ for target in del_list:
98
+ del repo_dict.ca.items[target]
99
+
100
+
101
+ def yaml_roundtrip_loader() -> YAML:
102
+ """Return Ruamel Roundtrip loader.
103
+
104
+ Returns
105
+ -------
106
+ YAML
107
+ Yaml Loader
108
+ """
109
+ yaml = YAML()
110
+ yaml.preserve_quotes = True
111
+ yaml.indent(mapping=2, sequence=4, offset=2)
112
+ return yaml
@@ -0,0 +1,176 @@
1
+ """Helper functions around the host system"""
2
+ import datetime
3
+ import logging
4
+ import os
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import List, Union
8
+
9
+ import click
10
+ import requests
11
+ from rich.console import Console
12
+ from rich.logging import RichHandler
13
+ from rich.prompt import Confirm
14
+ from rich.table import Table
15
+ from rich.traceback import install as install_rich_traceback
16
+
17
+ from . import cli as godoo_cli_helpers
18
+
19
+ LOGGER = logging.getLogger(__name__)
20
+
21
+
22
+ def run_cmd(command: str, log_name: str = "", **kwargs) -> subprocess.CompletedProcess:
23
+ """Runs command via subprocess.run
24
+
25
+ Parameters
26
+ ----------
27
+ command : str
28
+ Command string
29
+ **kwargs
30
+ get passed down to Run
31
+
32
+ Returns
33
+ -------
34
+ CompletedProcess
35
+ """
36
+ LOGGER.debug("Running shell: '%s'", command)
37
+ if not kwargs.get("shell"):
38
+ kwargs["shell"] = True
39
+ proc = subprocess.run(command, **kwargs)
40
+ if proc.returncode != 0 and log_name:
41
+ LOGGER.error("Failed: %s", log_name)
42
+ return proc
43
+
44
+
45
+ def ensure_dotenv(varname: str) -> str:
46
+ """
47
+ Load Env Var.
48
+ Raise Error if not Set.
49
+
50
+ Parameters
51
+ ----------
52
+ varname : str
53
+ env var name
54
+
55
+ Returns
56
+ -------
57
+ str
58
+ env value
59
+
60
+ Raises
61
+ ------
62
+ ReferenceError
63
+ if env var is none
64
+ """
65
+ var = os.getenv(varname)
66
+ if var is None:
67
+ raise ReferenceError(f"Env Variable: {varname} is not set")
68
+ return var
69
+
70
+
71
+ def set_logging(verbose: bool = False) -> None:
72
+ """
73
+ Set the Logging Config according to passed arguments.
74
+
75
+ Parameters
76
+ ----------
77
+ verbose : bool
78
+ Wether to Log debug Messages
79
+ """
80
+ if verbose:
81
+ install_rich_traceback(suppress=[click, godoo_cli_helpers])
82
+ logging.basicConfig(
83
+ level=logging.DEBUG,
84
+ format="{message}",
85
+ style="{",
86
+ handlers=[
87
+ RichHandler(
88
+ level=logging.DEBUG,
89
+ show_path=True,
90
+ rich_tracebacks=True,
91
+ tracebacks_show_locals=True,
92
+ )
93
+ ],
94
+ datefmt="%Y-%m-%d %H:%M:%S",
95
+ )
96
+ else:
97
+ logging.basicConfig(
98
+ level=logging.INFO,
99
+ format="{message}",
100
+ style="{",
101
+ handlers=[RichHandler(level=logging.INFO, show_path=False, rich_tracebacks=False)],
102
+ datefmt="%Y-%m-%d %H:%M:%S",
103
+ )
104
+
105
+
106
+ def download_file(url: str, save_path: Path, chunk_size: int = 128) -> None:
107
+ """Download file from URL.
108
+
109
+ Parameters
110
+ ----------
111
+ url : _type_
112
+ url to get file from
113
+ save_path : _type_
114
+ Where to save the file
115
+ chunk_size : int, optional
116
+ Chunk size to iterate over request, by default 128
117
+ """
118
+ LOGGER.debug("Downloading File: '%s' to '%s'", url, save_path)
119
+ r = requests.get(url, stream=True)
120
+ with open(save_path, "wb") as fd:
121
+ for chunk in r.iter_content(chunk_size=chunk_size):
122
+ fd.write(chunk)
123
+
124
+
125
+ def typer_ask_overwrite_path(paths: Union[List[Path], Path]) -> bool:
126
+ """Checks if the provided Paths do already exist.
127
+ Ignores 0 size files and empty folders.
128
+ Prints table of Paths with size and Changedate.
129
+ Prompts user to continue or abort typer.
130
+
131
+ Returns
132
+ ---------
133
+ False, when we shall not overwrite files.
134
+ True, when there are no files to override or we should override
135
+ """
136
+ if isinstance(paths, Path):
137
+ paths = [paths]
138
+
139
+ def path_exists(path: Path):
140
+ """If Path exists and is no empty dir"""
141
+ if path.is_dir():
142
+ return bool(path.glob("*"))
143
+ else:
144
+ return path.exists() and path.stat().st_size
145
+
146
+ existing_paths = [p for p in paths if path_exists(p)]
147
+ if not existing_paths:
148
+ return True
149
+ table = Table()
150
+ table.add_column("Name")
151
+ table.add_column("Size", justify="right")
152
+ table.add_column("Date Changed")
153
+
154
+ def file_or_folder_size_mb(path: Path) -> float:
155
+ """Get size of file or all files in folder summed in MB"""
156
+
157
+ def file_size_mb(file: Path) -> float:
158
+ """Get size of file in MB"""
159
+ return file.stat().st_size / (1024 * 1024)
160
+
161
+ if path.is_file():
162
+ return file_size_mb(path)
163
+ else:
164
+ return sum([file_size_mb(f) for f in path.rglob("*")])
165
+
166
+ for p in existing_paths:
167
+ timestamp = datetime.datetime.fromtimestamp(p.stat().st_mtime)
168
+ path_size = round(file_or_folder_size_mb(p), 2)
169
+ table.add_row(p.name, f"{path_size}mb", timestamp.strftime("%Y-%m-%d: %H:%M:%S"))
170
+ LOGGER.warning("Found Existing Odoo Files:")
171
+ Console().print(table)
172
+ override = Confirm.ask("override?")
173
+ if override:
174
+ return True
175
+ LOGGER.info("Aborting")
176
+ return False
godoo_cli/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.7.0"