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,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.odoo_manifest 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 manifest. 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,207 @@
1
+ """ Module to provide GIT interaction."""
2
+ import logging
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from git import GitCommandError, InvalidGitRepositoryError, Repo
7
+
8
+ from .git_url import GitUrl
9
+ from .zip_download import git_download_zip
10
+
11
+ LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ def _git_clean_clone(repo_src: str, target_folder: Path, **kwargs):
15
+ """Clears targetfolder and does a clean clone_from
16
+
17
+ Parameters
18
+ ----------
19
+ repo_src : str
20
+ Clone url
21
+ target_folder : Path
22
+ Download folder
23
+ branch : str
24
+ branch to clone
25
+
26
+ Returns
27
+ -------
28
+ _type_re
29
+ _description_
30
+ """
31
+ LOGGER.debug("Cloning Repo: %s, to '%s', Kwargs: '%s'", repo_src, target_folder, kwargs)
32
+ if not isinstance(target_folder, Path):
33
+ target_folder = Path(target_folder)
34
+ if target_folder.exists():
35
+ LOGGER.debug("Clearing Repo folder: %s", target_folder)
36
+ shutil.rmtree(target_folder)
37
+ return Repo.clone_from(repo_src, target_folder, **kwargs)
38
+
39
+
40
+ def git_pull_checkout_reset(
41
+ repo: Repo, branch: str = "master", commit: str = "", pull: str = "", reset_hard: bool = True
42
+ ):
43
+ """
44
+ Ensure git Repo is on specific branch and commit.
45
+ Will unshallow if neccessary
46
+
47
+ Parameters
48
+ ----------
49
+ repo : Repo
50
+ Git Repo
51
+ branch : str, optional
52
+ branch to checkout, by default "master"
53
+ commit : str, optional
54
+ specific commitSHA to checkout, by default ""
55
+ pull : str, optional
56
+ specific target to pull. Usually used in conjunction with branch.
57
+ reset_hard : bool, optional
58
+ will call reset --hard before checkout to ensure clean git repo, by default True
59
+ """
60
+
61
+ if reset_hard:
62
+ repo.git.reset("--hard", "HEAD")
63
+
64
+ if pull:
65
+ LOGGER.debug("Pulling Repo: %s, %s", repo.working_dir, pull)
66
+ try:
67
+ repo.remotes[0].pull(pull)
68
+ except GitCommandError as e:
69
+ if "fatal: refusing to merge unrelated histories" in e.stderr:
70
+ clone_kwargs = {}
71
+ if branch:
72
+ clone_kwargs["branch"] = branch
73
+ repo = _git_clean_clone(repo.remotes[0].url, repo.working_dir, **clone_kwargs)
74
+ else:
75
+ raise e
76
+ if commit:
77
+ if str(repo.head.commit) != str(commit):
78
+ LOGGER.debug("Checking out %s to Commit: %s", repo.git_dir, commit)
79
+ repo.git.checkout(commit)
80
+ return
81
+ if branch:
82
+ LOGGER.debug("Checking Out repo %s to Branch: %s", repo.git_dir, branch)
83
+ repo.git.checkout(branch)
84
+
85
+
86
+ def git_ensure_ref(
87
+ target_folder: Path,
88
+ repo_src: str,
89
+ branch: str = "master",
90
+ commit: str = "",
91
+ pull: str = "",
92
+ **kwargs,
93
+ ):
94
+ """
95
+ Clone a git Repo and ensure its HEAD is set to Branch and commit.
96
+
97
+
98
+ Parameters
99
+ ----------
100
+ target_folder : Path
101
+ "target folder name"
102
+ repo_src : str
103
+ "repo source url"
104
+ commit : str, optional
105
+ commit on which to set the head
106
+ pull : str, optional
107
+ specific target to pull. Usually used in conjunction with branch.
108
+ branch : str, optional
109
+ branch on which to set head, by default 'master'
110
+ **kwargs
111
+ get passed to git clone
112
+ """
113
+ LOGGER.info("Ensuring Repo '%s' --> '%s'", repo_src, target_folder)
114
+ target_folder.mkdir(exist_ok=True, parents=True)
115
+ try:
116
+ repo = Repo(target_folder)
117
+ current = str(repo.head.commit)
118
+
119
+ if not pull and commit and str(commit) != str(current):
120
+ pull = str(commit)
121
+
122
+ if not pull and branch and not commit:
123
+ remote_name = str(repo.remotes[0].name)
124
+ try:
125
+ remote_commit = repo.git.rev_parse(remote_name + "/" + str(branch))
126
+ except Exception:
127
+ remote_commit = "unknown"
128
+ LOGGER.debug(
129
+ "Repo: %s comparing local head '%s' with remote head '%s'", repo.working_dir, current, remote_commit
130
+ )
131
+ if str(remote_commit) != current:
132
+ pull = str(branch)
133
+
134
+ git_pull_checkout_reset(repo=repo, branch=branch, commit=commit, pull=pull)
135
+
136
+ if current == str(repo.head.commit):
137
+ LOGGER.info("Repo Commit matches. Skipping: '%s' --> '%s'", repo_src, target_folder)
138
+ else:
139
+ LOGGER.info("Pulled Repo from: '%s'. Head is now at: %s", repo_src, repo.head.commit)
140
+ except InvalidGitRepositoryError:
141
+ repo = _git_clean_clone(repo_src, target_folder, branch=branch, **kwargs)
142
+ git_pull_checkout_reset(repo=repo, branch=branch, commit=commit, pull=False)
143
+
144
+ LOGGER.info(
145
+ "Cloned Repo: '%s'. Branch='%s' Head='%s'",
146
+ repo_src,
147
+ repo.active_branch.name if not repo.head.is_detached else "Detached",
148
+ repo.head.commit,
149
+ )
150
+ return repo_src, repo.head.commit
151
+
152
+
153
+ def git_ensure_repo(
154
+ target_folder: Path,
155
+ repo_src: str,
156
+ branch: str = "master",
157
+ commit: str = "",
158
+ pull: str = "",
159
+ zip_mode: bool = False,
160
+ **kwargs,
161
+ ):
162
+ """Ensures git remote contents are in folder.
163
+
164
+ Parameters
165
+ ----------
166
+ target_folder : Path
167
+ Folder to clone/download into
168
+ repo_src : str
169
+ Git Remote Url
170
+ branch : str, optional
171
+ target branch, by default "master"
172
+ commit : str, optional
173
+ target ref, by default ""
174
+ pull : str, optional
175
+ ref to pull, by default ""
176
+ zip_mode : bool, optional
177
+ wether to download a zip (fast but no commit history) or clone and fetch (fully working git clone), by default False
178
+ **kwargs
179
+ get passed to git clone
180
+ """
181
+
182
+ if isinstance(target_folder, str):
183
+ target_folder = Path(target_folder)
184
+
185
+ if target_folder.exists() and any(target_folder.iterdir()) and not target_folder.glob(".git"):
186
+ LOGGER.info("Assuming '%s' got pulled in .Zip mode. Skipping Clone and commit check.", target_folder)
187
+ zip_mode = True
188
+
189
+ if zip_mode and GitUrl(repo_src).url_type == "ssh":
190
+ LOGGER.info("Zip downloading currently not supported for SSH type Urls")
191
+ zip_mode = False
192
+
193
+ if zip_mode:
194
+ return git_download_zip(
195
+ target_folder=target_folder,
196
+ repo_url=repo_src,
197
+ branch=branch,
198
+ commit=commit,
199
+ )
200
+ return git_ensure_ref(
201
+ target_folder=target_folder,
202
+ repo_src=repo_src,
203
+ branch=branch,
204
+ commit=commit,
205
+ pull=pull,
206
+ **kwargs,
207
+ )
@@ -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,49 @@
1
+ import logging
2
+ import shutil
3
+ import tempfile
4
+ import zipfile
5
+ from pathlib import Path
6
+
7
+ from ..helpers.system import download_file
8
+ from .git_url import GitUrl
9
+
10
+ LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ def git_download_zip(repo_url: str, target_folder: Path, branch: str, commit: str = ""):
14
+ """Download Repo Zip from Github.
15
+
16
+ Parameters
17
+ ----------
18
+ repo_url : str
19
+ Github Repo Url
20
+ target_folder : Path
21
+ Download Target
22
+ branch : str
23
+ BRanch to download
24
+ commit : str, optional
25
+ Specific Commit to download, by default ""
26
+
27
+ Raises
28
+ ------
29
+ FileNotFoundError
30
+ If Download failed
31
+ """
32
+ git_url = GitUrl(repo_url)
33
+ download_url = git_url.get_archive_url(ref=commit or branch)
34
+ with tempfile.TemporaryDirectory() as tdir:
35
+ zip_path = Path(tdir) / f"{git_url.name}.zip"
36
+ LOGGER.info("Downloading GitRepo Zip: '%s'", download_url)
37
+ LOGGER.debug("Target Path: '%s' ", zip_path)
38
+ download_file(download_url, zip_path)
39
+ if not zip_path.exists():
40
+ raise FileNotFoundError(f"Could not download Repo Zip from: {download_url}")
41
+ LOGGER.info("Extracting GitRepo Zip: %s", git_url.name)
42
+ ex_location = Path(tdir) / "extract"
43
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
44
+ zip_ref.extractall(ex_location)
45
+ for path in ex_location.glob("*"):
46
+ LOGGER.info("Moving %s to %s", path.stem, target_folder)
47
+ shutil.rmtree(target_folder, ignore_errors=True)
48
+ path.rename(target_folder)
49
+ break
@@ -0,0 +1 @@
1
+ """Provides Various Helper Functions for Odoo"""
@@ -0,0 +1,61 @@
1
+ import logging
2
+ import re
3
+ import subprocess
4
+ from ast import literal_eval
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ from .odoo_files import _get_python_requirements_of_modules
9
+ from .system import run_cmd
10
+
11
+ LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ def _install_py_reqs_for_modules(addon_paths: List[Path], module_names: List[str] = None):
15
+ """Install Python Requirements mentioned in odoo module manifests of given modules
16
+
17
+ Parameters
18
+ ----------
19
+ addon_paths : List[Path]
20
+ odoo-bin addons path
21
+ module_names : List[str], Optional
22
+ List of modules to filter for, by default "all available modules"
23
+
24
+ Returns
25
+ -------
26
+ CompletedProcess
27
+ """
28
+ py_reqs = _get_python_requirements_of_modules(addon_paths=addon_paths, filter_module_names=module_names)
29
+ if py_reqs:
30
+ installed_packages = run_cmd(
31
+ "pip list --format json --disable-pip-version-check", check=True, shell=True, stdout=subprocess.PIPE
32
+ ).stdout.decode("utf-8")
33
+ installed_packages = [m.get("name") for m in literal_eval(installed_packages)]
34
+ if missing_packages := [p for p in py_reqs if p not in installed_packages]:
35
+ LOGGER.info("Installing Python requirements: %s", missing_packages)
36
+ res = run_cmd(f"pip install {' '.join(missing_packages)} --disable-pip-version-check", shell=True)
37
+ if res.returncode != 0:
38
+ raise FileNotFoundError("Pip installation error at: %s" % missing_packages)
39
+ return res
40
+ LOGGER.debug("No py Requirements to be installed")
41
+
42
+
43
+ def _install_py_reqs_by_odoo_cmd(addon_paths: List[Path], odoo_bin_cmd: str):
44
+ """Install Python reqs for modules mentioned in odoo-bin commandline --init or -i directives.
45
+
46
+ Parameters
47
+ ----------
48
+ addon_paths : List[Path]
49
+ odoo-bin addons-path
50
+ odoo_bin_cmd : str
51
+ odoo-bin commandline
52
+
53
+ Returns
54
+ -------
55
+ CompletedProcess
56
+ """
57
+ install_modules = []
58
+ for m in re.finditer(r'(--init|-i) "?([^ \n]+)"?', odoo_bin_cmd):
59
+ install_modules += m.group(2).split(",")
60
+ if install_modules:
61
+ return _install_py_reqs_for_modules(addon_paths=addon_paths, module_names=install_modules)
@@ -0,0 +1,15 @@
1
+ import logging
2
+ import os
3
+
4
+ LOGGER = logging.getLogger(__name__)
5
+
6
+
7
+ def check_dangerous_command():
8
+ isdev = str(os.getenv("WORKSPACE_IS_DEV"))
9
+ if not isdev.lower() == "true":
10
+ LOGGER.warning(
11
+ """This function is dangerous in Production environments.
12
+ Please set 'WORKSPACE_IS_DEV=true' as environment Variable to continue
13
+ """
14
+ )
15
+ exit(1)