gOdoo-cli 0.0.2__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.
- godoo_cli/__init__.py +2 -0
- godoo_cli/__main__.py +5 -0
- godoo_cli/cli.py +78 -0
- godoo_cli/commands/__init__.py +7 -0
- godoo_cli/commands/bootstrap.py +183 -0
- godoo_cli/commands/launch.py +188 -0
- godoo_cli/commands/makedev.py +138 -0
- godoo_cli/commands/rpc/__init__.py +8 -0
- godoo_cli/commands/rpc/cli.py +19 -0
- godoo_cli/commands/rpc/cli_callbacks.py +37 -0
- godoo_cli/commands/rpc/importer.py +70 -0
- godoo_cli/commands/rpc/modules.py +108 -0
- godoo_cli/commands/rpc/translations.py +94 -0
- godoo_cli/commands/shell.py +42 -0
- godoo_cli/commands/source_get.py +164 -0
- godoo_cli/commands/test.py +91 -0
- godoo_cli/git/__init__.py +4 -0
- godoo_cli/git/git_odoo.py +45 -0
- godoo_cli/git/git_odoo_addons.py +95 -0
- godoo_cli/git/git_repo.py +157 -0
- godoo_cli/git/git_url.py +152 -0
- godoo_cli/git/zip_download.py +47 -0
- godoo_cli/helpers/__init__.py +83 -0
- godoo_cli/helpers/cli.py +85 -0
- godoo_cli/helpers/odoo_files.py +105 -0
- godoo_cli/helpers/repospec.py +112 -0
- godoo_cli-0.0.2.dist-info/LICENSE +165 -0
- godoo_cli-0.0.2.dist-info/METADATA +169 -0
- godoo_cli-0.0.2.dist-info/RECORD +31 -0
- godoo_cli-0.0.2.dist-info/WHEEL +4 -0
- godoo_cli-0.0.2.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from git import Commit
|
|
7
|
+
|
|
8
|
+
from ..helpers.repospec import update_yml, yaml_roundtrip_loader
|
|
9
|
+
from .git_repo import git_ensure_repo
|
|
10
|
+
from .git_url import GitUrl
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def git_ensure_addon_repos(
|
|
16
|
+
root_folder: Path,
|
|
17
|
+
git_yml_path: Path,
|
|
18
|
+
generate_yml_compare_comments: bool = False,
|
|
19
|
+
download_archive: bool = False,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Clone repos specified in Yml.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
root_folder : Path
|
|
27
|
+
clone target folder
|
|
28
|
+
git_yml_path : Path
|
|
29
|
+
yml describing what to clone
|
|
30
|
+
generate_yml_compare_comments : bool, optional
|
|
31
|
+
wether to add three dot compare on remote to repo urls
|
|
32
|
+
download_archive : bool, optional
|
|
33
|
+
wether to download as .zip (fast but no history), by default False
|
|
34
|
+
"""
|
|
35
|
+
yaml = yaml_roundtrip_loader()
|
|
36
|
+
git_repos = yaml.load(git_yml_path.resolve())
|
|
37
|
+
if _git_clone_addon_repos(root_folder=root_folder, git_repos=git_repos, download_archive=download_archive):
|
|
38
|
+
update_yml(git_repos, generate_yml_compare_comments)
|
|
39
|
+
LOGGER.info("Updating Git Thirdparty Repo Commit hashes")
|
|
40
|
+
yaml.dump(git_repos, git_yml_path)
|
|
41
|
+
|
|
42
|
+
return git_repos
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _git_clone_addon_repos(
|
|
46
|
+
root_folder: Path,
|
|
47
|
+
git_repos: Dict[str, Dict[str, str]],
|
|
48
|
+
download_archive: bool = False,
|
|
49
|
+
) -> Dict[str, Commit]:
|
|
50
|
+
"""
|
|
51
|
+
Clones Git repos specified in dict into Root folder.
|
|
52
|
+
Ensures repo names are prefixed and uses 8 threads to clone.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
root_folder : Path
|
|
57
|
+
clone target folder
|
|
58
|
+
git_repos : Dict[str, Dict[str, str]]
|
|
59
|
+
dict of {parent_folder_name:{"url":clone_url,"branch":clone_branch,"commit":specific_commit_to_clone}}
|
|
60
|
+
branch and commit are optional
|
|
61
|
+
branch defaults to odoo branch from spec file
|
|
62
|
+
download_archive : bool, optional
|
|
63
|
+
wether to download as .zip (fast but no history), by default False
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
Dict[str:Commit]
|
|
67
|
+
dict of {git_src_url:HeadCommit}
|
|
68
|
+
"""
|
|
69
|
+
default_branch = git_repos["odoo"].get("branch")
|
|
70
|
+
LOGGER.info("Cloning Thirdparty Addons source.")
|
|
71
|
+
with concurrent.futures.ThreadPoolExecutor(8) as executor:
|
|
72
|
+
futures = []
|
|
73
|
+
thirdparty_repos = git_repos.get("thirdparty")
|
|
74
|
+
if not thirdparty_repos:
|
|
75
|
+
LOGGER.info("No Thirdparty Key in Repospec. Skipping...")
|
|
76
|
+
return
|
|
77
|
+
for prefix, repos in thirdparty_repos.items():
|
|
78
|
+
for repo in repos:
|
|
79
|
+
repo_url = GitUrl(repo["url"])
|
|
80
|
+
name = f"{prefix}_{repo_url.name}"
|
|
81
|
+
futures.append(
|
|
82
|
+
executor.submit(
|
|
83
|
+
git_ensure_repo,
|
|
84
|
+
target_folder=Path(root_folder / name),
|
|
85
|
+
repo_src=repo_url.url,
|
|
86
|
+
branch=repo.get("branch", default_branch),
|
|
87
|
+
commit=repo.get("commit"),
|
|
88
|
+
zip_mode=download_archive,
|
|
89
|
+
filter="blob:none",
|
|
90
|
+
single_branch=True,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
clone_results = [f.result() for f in futures]
|
|
94
|
+
clone_results = {r[0]: r[1] for r in clone_results if r}
|
|
95
|
+
return clone_results
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from git import InvalidGitRepositoryError, Repo
|
|
5
|
+
|
|
6
|
+
from .zip_download import git_download_zip
|
|
7
|
+
|
|
8
|
+
LOGGER = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def git_pull_checkout_reset(
|
|
12
|
+
repo: Repo, branch: str = "master", commit: str = "", pull: str = "", reset_hard: bool = True
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Ensure git Repo is on specific branch and commit.
|
|
16
|
+
Will unshallow if neccessary
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
repo : Repo
|
|
21
|
+
Git Repo
|
|
22
|
+
branch : str, optional
|
|
23
|
+
branch to checkout, by default "master"
|
|
24
|
+
commit : str, optional
|
|
25
|
+
specific commitSHA to checkout, by default ""
|
|
26
|
+
pull : str, optional
|
|
27
|
+
specific target to pull. Usually used in conjunction with branch.
|
|
28
|
+
reset_hard : bool, optional
|
|
29
|
+
will call reset --hard after checkout to ensure clean git repo, by default True
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
if pull:
|
|
34
|
+
LOGGER.debug("Pulling Repo: %s, %s", repo.git_dir, pull)
|
|
35
|
+
repo.remotes[0].pull(pull)
|
|
36
|
+
if commit:
|
|
37
|
+
if str(repo.head.commit) != str(commit):
|
|
38
|
+
LOGGER.debug("Checking out %s to Commit: %s", repo.git_dir, commit)
|
|
39
|
+
repo.git.checkout(commit)
|
|
40
|
+
return
|
|
41
|
+
if branch:
|
|
42
|
+
LOGGER.debug("Checking Out repo %s to Branch: %s", repo.git_dir, branch)
|
|
43
|
+
repo.git.checkout(branch)
|
|
44
|
+
if reset_hard:
|
|
45
|
+
repo.git.reset("--hard")
|
|
46
|
+
except Exception as e:
|
|
47
|
+
raise Exception(
|
|
48
|
+
str(f"Error while ensuring Git repo: {str(repo)}, branch='{branch}', commit='{commit}', fetch='{pull}'")
|
|
49
|
+
) from e
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def git_ensure_ref(
|
|
53
|
+
target_folder: Path,
|
|
54
|
+
repo_src: str,
|
|
55
|
+
branch: str = "master",
|
|
56
|
+
commit: str = "",
|
|
57
|
+
pull: str = "",
|
|
58
|
+
**kwargs,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Clone a git Repo and ensure its HEAD is set to Branch and commit.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
target_folder : Path
|
|
67
|
+
"target folder name"
|
|
68
|
+
repo_src : str
|
|
69
|
+
"repo source url"
|
|
70
|
+
commit : str, optional
|
|
71
|
+
commit on which to set the head
|
|
72
|
+
pull : str, optional
|
|
73
|
+
specific target to pull. Usually used in conjunction with branch.
|
|
74
|
+
branch : str, optional
|
|
75
|
+
branch on which to set head, by default 'master'
|
|
76
|
+
**kwargs
|
|
77
|
+
get passed to git clone
|
|
78
|
+
"""
|
|
79
|
+
LOGGER.info("Ensuring Repo '%s' --> '%s'", repo_src, target_folder)
|
|
80
|
+
target_folder.mkdir(exist_ok=True, parents=True)
|
|
81
|
+
try:
|
|
82
|
+
repo = Repo(target_folder)
|
|
83
|
+
current = repo.head.commit
|
|
84
|
+
|
|
85
|
+
if not pull and commit and str(commit) != str(current):
|
|
86
|
+
pull = str(commit)
|
|
87
|
+
|
|
88
|
+
if not pull and branch and not commit:
|
|
89
|
+
pull = str(branch)
|
|
90
|
+
git_pull_checkout_reset(repo=repo, branch=branch, commit=commit, pull=pull)
|
|
91
|
+
|
|
92
|
+
if current == repo.head.commit:
|
|
93
|
+
LOGGER.info("Repo Commit matches. Skipping: '%s' --> '%s'", repo_src, target_folder)
|
|
94
|
+
else:
|
|
95
|
+
LOGGER.info("Pulled Repo from: '%s'. Head is now at: %s", repo_src, repo.head.commit)
|
|
96
|
+
except InvalidGitRepositoryError:
|
|
97
|
+
LOGGER.debug("Cloning Repo: %s, to '%s', branch '%s'", repo_src, target_folder, branch)
|
|
98
|
+
repo = Repo.clone_from(repo_src, target_folder, branch=branch, **kwargs)
|
|
99
|
+
git_pull_checkout_reset(repo=repo, branch=branch, commit=commit, pull=pull)
|
|
100
|
+
|
|
101
|
+
LOGGER.info(
|
|
102
|
+
"Cloned Repo: '%s'. Branch='%s' Head='%s'",
|
|
103
|
+
repo_src,
|
|
104
|
+
repo.active_branch.name if not repo.head.is_detached else "Detached",
|
|
105
|
+
repo.head.commit,
|
|
106
|
+
)
|
|
107
|
+
return repo_src, repo.head.commit
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def git_ensure_repo(
|
|
111
|
+
target_folder: Path,
|
|
112
|
+
repo_src: str,
|
|
113
|
+
branch: str = "master",
|
|
114
|
+
commit: str = "",
|
|
115
|
+
pull: str = "",
|
|
116
|
+
zip_mode: bool = False,
|
|
117
|
+
**kwargs,
|
|
118
|
+
):
|
|
119
|
+
"""Ensures git remote contents are in folder.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
target_folder : Path
|
|
124
|
+
Folder to clone/download into
|
|
125
|
+
repo_src : str
|
|
126
|
+
Git Remote Url
|
|
127
|
+
branch : str, optional
|
|
128
|
+
target branch, by default "master"
|
|
129
|
+
commit : str, optional
|
|
130
|
+
target ref, by default ""
|
|
131
|
+
pull : str, optional
|
|
132
|
+
ref to pull, by default ""
|
|
133
|
+
zip_mode : bool, optional
|
|
134
|
+
wether to download a zip (fast but no commit history) or clone and fetch (fully working git clone), by default False
|
|
135
|
+
**kwargs
|
|
136
|
+
get passed to git clone
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
if target_folder.exists() and any(target_folder.iterdir()) and not target_folder.glob(".git"):
|
|
140
|
+
LOGGER.info("Assuming '%s' got pulled in .Zip mode. Skipping Clone and commit check.", target_folder)
|
|
141
|
+
zip_mode = True
|
|
142
|
+
|
|
143
|
+
if zip_mode:
|
|
144
|
+
return git_download_zip(
|
|
145
|
+
target_folder=target_folder,
|
|
146
|
+
repo_url=repo_src,
|
|
147
|
+
branch=branch,
|
|
148
|
+
commit=commit,
|
|
149
|
+
)
|
|
150
|
+
return git_ensure_ref(
|
|
151
|
+
target_folder=target_folder,
|
|
152
|
+
repo_src=repo_src,
|
|
153
|
+
branch=branch,
|
|
154
|
+
commit=commit,
|
|
155
|
+
pull=pull,
|
|
156
|
+
**kwargs,
|
|
157
|
+
)
|
godoo_cli/git/git_url.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GitRemoteType(Enum):
|
|
7
|
+
gitlab = "gitlab"
|
|
8
|
+
github = "github"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GitUrl:
|
|
12
|
+
"""Class to Structurize Git URLs.
|
|
13
|
+
Works with SSH and HTTP(s) URLs
|
|
14
|
+
Can generate Compare Urls
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
url: str
|
|
18
|
+
url_type: Literal["http", "ssh"]
|
|
19
|
+
domain: str
|
|
20
|
+
path: str
|
|
21
|
+
user: str
|
|
22
|
+
port: int
|
|
23
|
+
name: str
|
|
24
|
+
|
|
25
|
+
def __init__(self, url: str) -> None:
|
|
26
|
+
self.url = url
|
|
27
|
+
if "http" in url:
|
|
28
|
+
http_regex = r"(?P<schema>https?):\/\/(?P<domain>[^\/]+)(?P<path>.*)"
|
|
29
|
+
http_match = re.search(http_regex, url)
|
|
30
|
+
self.url_type = http_match.group("schema")
|
|
31
|
+
self.domain = http_match.group("domain")
|
|
32
|
+
self.path = http_match.group("path")
|
|
33
|
+
else:
|
|
34
|
+
ssh_regex = r"(?P<user>\w+)@(?P<domain>[^:]+):(?:(?P<port>\d+)]?:)?(?P<path>.*)"
|
|
35
|
+
ssh_match = re.search(ssh_regex, url)
|
|
36
|
+
self.url_type = "ssh"
|
|
37
|
+
self.domain = ssh_match.group("domain")
|
|
38
|
+
self.path = ssh_match.group("path")
|
|
39
|
+
self.user = ssh_match.group("user")
|
|
40
|
+
self.port = ssh_match.group("port")
|
|
41
|
+
|
|
42
|
+
if self.path.endswith(".git"): # Todo Python 3.9 Use Removesuffix and RemovePrefix
|
|
43
|
+
self.path = self.path[:-4]
|
|
44
|
+
if self.path.endswith("/"):
|
|
45
|
+
self.path = self.path[:-1]
|
|
46
|
+
if self.path.startswith("/"):
|
|
47
|
+
self.path = self.path[1:]
|
|
48
|
+
self.name = self.path.split("/")[-1]
|
|
49
|
+
|
|
50
|
+
def _clean_http_url(self) -> str:
|
|
51
|
+
"""Return HTTPs URL without .git suffix.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
str
|
|
56
|
+
Https:// url
|
|
57
|
+
"""
|
|
58
|
+
return f"https://{self.domain}/{self.path}"
|
|
59
|
+
|
|
60
|
+
def _git_type(self) -> GitRemoteType:
|
|
61
|
+
"""Get git Remote type.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
Literal
|
|
66
|
+
"github", "gitlab"
|
|
67
|
+
|
|
68
|
+
Raises
|
|
69
|
+
------
|
|
70
|
+
ValueError
|
|
71
|
+
If Type cannot be determined.
|
|
72
|
+
"""
|
|
73
|
+
if "gitlab" in self.domain:
|
|
74
|
+
return GitRemoteType.gitlab
|
|
75
|
+
elif "github" in self.domain:
|
|
76
|
+
return GitRemoteType.github
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError(f"Cant get Git Service type from {self.domain}")
|
|
79
|
+
|
|
80
|
+
def get_compare_url(self, from_compare: str, to_compare: str) -> str:
|
|
81
|
+
"""Get Compare url between two Refs.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
from_compare : str
|
|
86
|
+
from compare ref
|
|
87
|
+
to_compare : str
|
|
88
|
+
to compare ref
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
str
|
|
93
|
+
Compare Url Like: https://github.com/odoo/odoo/compare/<commitSHA>...<branchName>
|
|
94
|
+
"""
|
|
95
|
+
remote_type = self._git_type()
|
|
96
|
+
if from_compare == to_compare:
|
|
97
|
+
return # Nothing to Compare here
|
|
98
|
+
http_url = self._clean_http_url()
|
|
99
|
+
if remote_type in [GitRemoteType.github, GitRemoteType.gitlab]:
|
|
100
|
+
return f"{http_url}/compare/{from_compare}...{to_compare}"
|
|
101
|
+
|
|
102
|
+
def get_archive_url(self, ref: str) -> str:
|
|
103
|
+
"""Get Download Url for Zip file.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
branch : str, optional
|
|
108
|
+
Repo Branch, by default ""
|
|
109
|
+
commit : str, optional
|
|
110
|
+
Repo download ref, by default ""
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
str
|
|
115
|
+
Url from which to download a zip file
|
|
116
|
+
|
|
117
|
+
Raises
|
|
118
|
+
------
|
|
119
|
+
ValueError
|
|
120
|
+
if ref is not specified
|
|
121
|
+
"""
|
|
122
|
+
if not ref:
|
|
123
|
+
raise ValueError("Missing either download ref (e.g. branch or commit) to generate Archive URL.")
|
|
124
|
+
http_url = self._clean_http_url()
|
|
125
|
+
remote_type = self._git_type()
|
|
126
|
+
if remote_type == GitRemoteType.github:
|
|
127
|
+
return f"{http_url}/archive/{ref }.zip"
|
|
128
|
+
if remote_type == GitRemoteType.gitlab:
|
|
129
|
+
return f"{http_url}/-/archive/{ref}/{self.name}.zip"
|
|
130
|
+
|
|
131
|
+
def get_file_raw_url(self, ref: str, file_path: str) -> str:
|
|
132
|
+
"""Gets the URL Pointing to the Raw file contents on the Remote.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
ref : str
|
|
137
|
+
Branch, Commit, Tag ,...
|
|
138
|
+
file_path : str
|
|
139
|
+
Relative file path in Repository
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
str
|
|
144
|
+
URL Pointing to the Raw file contents on the Remote
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
http_url = self._clean_http_url()
|
|
148
|
+
remote_type = self._git_type()
|
|
149
|
+
if remote_type == GitRemoteType.github:
|
|
150
|
+
return f"{http_url.replace(self.domain,'raw.githubusercontent.com')}/{ref}/{file_path}"
|
|
151
|
+
if remote_type == GitRemoteType.gitlab:
|
|
152
|
+
return f"{http_url}/-/raw/{ref}/{file_path}"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import tempfile
|
|
3
|
+
import zipfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..helpers import download_file
|
|
7
|
+
from .git_url import GitUrl
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def git_download_zip(repo_url: str, target_folder: Path, branch: str, commit: str = ""):
|
|
13
|
+
"""Download Repo Zip from Github.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
repo_url : str
|
|
18
|
+
Github Repo Url
|
|
19
|
+
target_folder : Path
|
|
20
|
+
Download Target
|
|
21
|
+
branch : str
|
|
22
|
+
BRanch to download
|
|
23
|
+
commit : str, optional
|
|
24
|
+
Specific Commit to download, by default ""
|
|
25
|
+
|
|
26
|
+
Raises
|
|
27
|
+
------
|
|
28
|
+
FileNotFoundError
|
|
29
|
+
If Download failed
|
|
30
|
+
"""
|
|
31
|
+
git_url = GitUrl(repo_url)
|
|
32
|
+
download_url = git_url.get_archive_url(ref=commit or branch)
|
|
33
|
+
with tempfile.TemporaryDirectory() as tdir:
|
|
34
|
+
zip_path = Path(tdir) / f"{git_url.name}.zip"
|
|
35
|
+
LOGGER.info("Downloading GitRepo Zip: '%s'", download_url)
|
|
36
|
+
LOGGER.debug("Target Path: '%s' ", zip_path)
|
|
37
|
+
download_file(download_url, zip_path)
|
|
38
|
+
if not zip_path.exists():
|
|
39
|
+
raise FileNotFoundError(f"Could not download Repo Zip from: {download_url}")
|
|
40
|
+
LOGGER.info("Extracting GitRepo Zip: %s", git_url.name)
|
|
41
|
+
ex_location = Path(tdir) / "extract"
|
|
42
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
|
43
|
+
zip_ref.extractall(ex_location)
|
|
44
|
+
for path in ex_location.glob("*"):
|
|
45
|
+
LOGGER.info("Moving %s to %s", path.stem, target_folder)
|
|
46
|
+
path.rename(target_folder)
|
|
47
|
+
break
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Provides Various Helper Functions for Odoo"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from rich.logging import RichHandler
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def ensure_dotenv(varname: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Load Env Var.
|
|
17
|
+
Raise Error if not Set.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
varname : str
|
|
22
|
+
env var name
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
str
|
|
27
|
+
env value
|
|
28
|
+
|
|
29
|
+
Raises
|
|
30
|
+
------
|
|
31
|
+
ReferenceError
|
|
32
|
+
if env var is none
|
|
33
|
+
"""
|
|
34
|
+
var = os.getenv(varname)
|
|
35
|
+
if var is None:
|
|
36
|
+
raise ReferenceError(f"Env Variable: {varname} is not set")
|
|
37
|
+
return var
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def set_logging(verbose: bool = False) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Set the Logging Config according to passed arguments.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
verbose : bool
|
|
47
|
+
Wether to Log debug Messages
|
|
48
|
+
"""
|
|
49
|
+
if verbose:
|
|
50
|
+
logging.basicConfig(
|
|
51
|
+
level=logging.DEBUG,
|
|
52
|
+
format="{message}",
|
|
53
|
+
style="{",
|
|
54
|
+
handlers=[RichHandler(level=logging.DEBUG, rich_tracebacks=True)],
|
|
55
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
logging.basicConfig(
|
|
59
|
+
level=logging.INFO,
|
|
60
|
+
format="{message}",
|
|
61
|
+
style="{",
|
|
62
|
+
handlers=[RichHandler(level=logging.INFO, show_path=False, rich_tracebacks=False)],
|
|
63
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def download_file(url: str, save_path: Path, chunk_size: int = 128) -> None:
|
|
68
|
+
"""Download file from URL.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
url : _type_
|
|
73
|
+
url to get file from
|
|
74
|
+
save_path : _type_
|
|
75
|
+
Where to save the file
|
|
76
|
+
chunk_size : int, optional
|
|
77
|
+
Chunk size to iterate over request, by default 128
|
|
78
|
+
"""
|
|
79
|
+
LOGGER.debug("Downloading File: '%s' to '%s'", url, save_path)
|
|
80
|
+
r = requests.get(url, stream=True)
|
|
81
|
+
with open(save_path, "wb") as fd:
|
|
82
|
+
for chunk in r.iter_content(chunk_size=chunk_size):
|
|
83
|
+
fd.write(chunk)
|
godoo_cli/helpers/cli.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from typer.models import ParameterInfo
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def typer_retuner(ret):
|
|
15
|
+
"""Exit Typer command with return code, if parent of calling function is not the typer_unpacker wrapper.
|
|
16
|
+
Will just return ret if ret is either not an integer, or the parent function was another python func.
|
|
17
|
+
|
|
18
|
+
Ensures, that we exit the CLI with a return code, only if the function is the CLI base command.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
ret : Any
|
|
23
|
+
Value to either return or Exit
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
Any
|
|
28
|
+
ret
|
|
29
|
+
"""
|
|
30
|
+
parent_func = sys._getframe(1).f_code.co_name
|
|
31
|
+
if isinstance(ret, int):
|
|
32
|
+
parent_name = sys._getframe(2).f_code.co_name
|
|
33
|
+
if parent_name != "typer_unwrapper":
|
|
34
|
+
LOGGER.debug("Exiting Typer Command: %s with code %s", parent_func, ret)
|
|
35
|
+
if ret > 255:
|
|
36
|
+
LOGGER.debug("Capping exit code to 255")
|
|
37
|
+
ret = 255
|
|
38
|
+
raise typer.Exit(code=ret)
|
|
39
|
+
LOGGER.debug("Returning Typer Command: %s", parent_func)
|
|
40
|
+
return ret
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def typer_unpacker(f: Callable):
|
|
44
|
+
"""
|
|
45
|
+
Apply as decorator to typer command function to make the function callable from python
|
|
46
|
+
if you used typer.Argument or typer.Option.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@functools.wraps(f)
|
|
51
|
+
def typer_unwrapper(*args, **kwargs):
|
|
52
|
+
# Get the default function argument that aren't passed in kwargs via the
|
|
53
|
+
# inspect module: https://stackoverflow.com/a/12627202
|
|
54
|
+
missing_default_values = {
|
|
55
|
+
k: (v.default, v._annotation)
|
|
56
|
+
for k, v in inspect.signature(f).parameters.items()
|
|
57
|
+
if v.default is not inspect.Parameter.empty and k not in kwargs
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for name, (func_default, target_type) in missing_default_values.items():
|
|
61
|
+
# If the default value is a typer.Option or typer.Argument, we have to
|
|
62
|
+
# pull either the .default attribute and pass it in the function
|
|
63
|
+
# invocation, or call it first.
|
|
64
|
+
if isinstance(func_default, ParameterInfo):
|
|
65
|
+
if ev := func_default.envvar:
|
|
66
|
+
env_var = os.getenv(ev)
|
|
67
|
+
try:
|
|
68
|
+
if env_res := target_type(env_var) if target_type else env_var:
|
|
69
|
+
kwargs[name] = env_res
|
|
70
|
+
continue
|
|
71
|
+
except TypeError as e:
|
|
72
|
+
raise TypeError(
|
|
73
|
+
f"Type mismatch in: '{ev}' Expected: '{target_type}' Got: '{type(env_var)}"
|
|
74
|
+
) from e
|
|
75
|
+
if callable(func_default.default):
|
|
76
|
+
kwargs[name] = func_default.default()
|
|
77
|
+
else:
|
|
78
|
+
if func_default.default is Ellipsis:
|
|
79
|
+
raise ValueError(f"Missing required Argument for: {name}")
|
|
80
|
+
kwargs[name] = func_default.default
|
|
81
|
+
|
|
82
|
+
# Call the wrapped function with the defaults injected if not specified.
|
|
83
|
+
return f(*args, **kwargs)
|
|
84
|
+
|
|
85
|
+
return typer_unwrapper
|