datavo-cli 0.22.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.
- datavo_cli/__init__.py +3 -0
- datavo_cli/client_factory.py +172 -0
- datavo_cli/commands/__init__.py +36 -0
- datavo_cli/commands/auth.py +94 -0
- datavo_cli/commands/config.py +93 -0
- datavo_cli/commands/dataset.py +198 -0
- datavo_cli/commands/events.py +48 -0
- datavo_cli/commands/jobpool.py +50 -0
- datavo_cli/commands/sample.py +29 -0
- datavo_cli/commands/sample_store.py +97 -0
- datavo_cli/commands/source_import.py +38 -0
- datavo_cli/commands/split_set.py +79 -0
- datavo_cli/commands/stats.py +22 -0
- datavo_cli/commands/team.py +191 -0
- datavo_cli/commands/user.py +67 -0
- datavo_cli/config_store.py +200 -0
- datavo_cli/diagnostics.py +77 -0
- datavo_cli/errors.py +49 -0
- datavo_cli/main.py +90 -0
- datavo_cli/parsers.py +411 -0
- datavo_cli/render.py +390 -0
- datavo_cli/utils.py +55 -0
- datavo_cli-0.22.0.dist-info/METADATA +51 -0
- datavo_cli-0.22.0.dist-info/RECORD +28 -0
- datavo_cli-0.22.0.dist-info/WHEEL +5 -0
- datavo_cli-0.22.0.dist-info/entry_points.txt +2 -0
- datavo_cli-0.22.0.dist-info/licenses/LICENSE +201 -0
- datavo_cli-0.22.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field, replace
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from datavo_sdk.client._transport import DEFAULT_SERVER_URL
|
|
8
|
+
from datavo_sdk.server_config import default_config_dir
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def config_path() -> Path:
|
|
12
|
+
# ``~/.datavo`` is resolved once, in the SDK (entra/plan_cache/server_config use it).
|
|
13
|
+
return default_config_dir() / "config.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class DatavoProfile:
|
|
18
|
+
api_base_url: str | None = None
|
|
19
|
+
auth_token: str | None = None
|
|
20
|
+
msal_tenant_id: str | None = None
|
|
21
|
+
msal_client_id: str | None = None
|
|
22
|
+
msal_scopes: list[str] | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class DatavoConfig:
|
|
27
|
+
active_profile: str | None = None
|
|
28
|
+
profiles: dict[str, DatavoProfile] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_profile(value: object) -> DatavoProfile:
|
|
32
|
+
if not isinstance(value, dict):
|
|
33
|
+
return DatavoProfile()
|
|
34
|
+
api_base_url = value.get("api_base_url")
|
|
35
|
+
auth_token = value.get("auth_token")
|
|
36
|
+
msal_tenant_id = value.get("msal_tenant_id")
|
|
37
|
+
msal_client_id = value.get("msal_client_id")
|
|
38
|
+
msal_scopes = value.get("msal_scopes")
|
|
39
|
+
if not isinstance(api_base_url, str):
|
|
40
|
+
api_base_url = None
|
|
41
|
+
if not isinstance(auth_token, str):
|
|
42
|
+
auth_token = None
|
|
43
|
+
if not isinstance(msal_tenant_id, str):
|
|
44
|
+
msal_tenant_id = None
|
|
45
|
+
if not isinstance(msal_client_id, str):
|
|
46
|
+
msal_client_id = None
|
|
47
|
+
if isinstance(msal_scopes, list):
|
|
48
|
+
msal_scopes = [scope for scope in msal_scopes if isinstance(scope, str) and scope.strip()]
|
|
49
|
+
else:
|
|
50
|
+
msal_scopes = None
|
|
51
|
+
return DatavoProfile(
|
|
52
|
+
api_base_url=api_base_url,
|
|
53
|
+
auth_token=auth_token,
|
|
54
|
+
msal_tenant_id=msal_tenant_id,
|
|
55
|
+
msal_client_id=msal_client_id,
|
|
56
|
+
msal_scopes=msal_scopes,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_config() -> DatavoConfig:
|
|
61
|
+
path = config_path()
|
|
62
|
+
if not path.exists():
|
|
63
|
+
return DatavoConfig()
|
|
64
|
+
try:
|
|
65
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
66
|
+
except (json.JSONDecodeError, OSError):
|
|
67
|
+
return DatavoConfig()
|
|
68
|
+
if not isinstance(payload, dict):
|
|
69
|
+
return DatavoConfig()
|
|
70
|
+
active_profile = payload.get("active_profile")
|
|
71
|
+
if not isinstance(active_profile, str):
|
|
72
|
+
active_profile = None
|
|
73
|
+
raw_profiles = payload.get("profiles")
|
|
74
|
+
profiles: dict[str, DatavoProfile] = {}
|
|
75
|
+
if isinstance(raw_profiles, dict):
|
|
76
|
+
for name, value in raw_profiles.items():
|
|
77
|
+
if isinstance(name, str) and name:
|
|
78
|
+
profiles[name] = _parse_profile(value)
|
|
79
|
+
return DatavoConfig(active_profile=active_profile, profiles=profiles)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def save_config(config: DatavoConfig) -> None:
|
|
83
|
+
path = config_path()
|
|
84
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
payload = {
|
|
86
|
+
"active_profile": config.active_profile,
|
|
87
|
+
"profiles": {
|
|
88
|
+
name: {
|
|
89
|
+
key: value
|
|
90
|
+
for key, value in {
|
|
91
|
+
"api_base_url": profile.api_base_url,
|
|
92
|
+
"auth_token": profile.auth_token,
|
|
93
|
+
"msal_tenant_id": profile.msal_tenant_id,
|
|
94
|
+
"msal_client_id": profile.msal_client_id,
|
|
95
|
+
"msal_scopes": profile.msal_scopes,
|
|
96
|
+
}.items()
|
|
97
|
+
if value is not None
|
|
98
|
+
}
|
|
99
|
+
for name, profile in sorted(config.profiles.items())
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
#: Where an unconfigured CLI points — the SDK's default, imported rather than restated
|
|
106
|
+
#: so the two cannot drift. Override via ``DATAVO_API_BASE_URL`` / ``datavo config set``.
|
|
107
|
+
DEFAULT_API_BASE_URL = DEFAULT_SERVER_URL
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_active_profile_name(config: DatavoConfig | None = None) -> str | None:
|
|
111
|
+
config = config or load_config()
|
|
112
|
+
env_name = (os.environ.get("DATAVO_PROFILE") or "").strip()
|
|
113
|
+
if env_name:
|
|
114
|
+
return env_name
|
|
115
|
+
return config.active_profile
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resolve_profile(config: DatavoConfig | None = None, *, profile_name: str | None = None) -> tuple[str, DatavoProfile]:
|
|
119
|
+
config = config or load_config()
|
|
120
|
+
profile_name = profile_name or get_active_profile_name(config)
|
|
121
|
+
if profile_name and profile_name in config.profiles:
|
|
122
|
+
return profile_name, config.profiles[profile_name]
|
|
123
|
+
env_url = (os.environ.get("DATAVO_API_BASE_URL") or "").strip()
|
|
124
|
+
if profile_name and not env_url:
|
|
125
|
+
# A named profile that does not exist is a typo, not an unconfigured CLI: fail
|
|
126
|
+
# rather than land somewhere the caller did not name.
|
|
127
|
+
raise RuntimeError(f"Unknown Datavo profile: {profile_name}")
|
|
128
|
+
# Unconfigured, or pointed by env. Start from the default instance and let
|
|
129
|
+
# apply_env_overrides layer DATAVO_API_BASE_URL over it — so the CLI runs out of the
|
|
130
|
+
# box, and one construction path serves both cases.
|
|
131
|
+
name = profile_name or ("env" if env_url else "prod")
|
|
132
|
+
return name, apply_env_overrides(DatavoProfile(api_base_url=DEFAULT_API_BASE_URL))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def apply_env_overrides(profile: DatavoProfile) -> DatavoProfile:
|
|
136
|
+
api_base_url = (os.environ.get("DATAVO_API_BASE_URL") or "").strip() or profile.api_base_url
|
|
137
|
+
auth_token = (os.environ.get("DATAVO_AUTH_TOKEN") or "").strip() or profile.auth_token
|
|
138
|
+
msal_tenant_id = (os.environ.get("DATAVO_ENTRA_TENANT_ID") or "").strip() or profile.msal_tenant_id
|
|
139
|
+
msal_client_id = (os.environ.get("DATAVO_ENTRA_CLIENT_ID") or "").strip() or profile.msal_client_id
|
|
140
|
+
env_audience = (os.environ.get("DATAVO_ENTRA_AUDIENCE") or "").strip()
|
|
141
|
+
env_scopes_raw = (os.environ.get("DATAVO_ENTRA_SCOPES") or "").strip()
|
|
142
|
+
msal_scopes = profile.msal_scopes
|
|
143
|
+
if env_scopes_raw:
|
|
144
|
+
msal_scopes = [scope.strip() for scope in env_scopes_raw.split(",") if scope.strip()]
|
|
145
|
+
elif env_audience:
|
|
146
|
+
msal_scopes = [f"{env_audience}/access_as_user" if env_audience.startswith("api://") else f"api://{env_audience}/access_as_user"]
|
|
147
|
+
return DatavoProfile(
|
|
148
|
+
api_base_url=api_base_url,
|
|
149
|
+
auth_token=auth_token,
|
|
150
|
+
msal_tenant_id=msal_tenant_id,
|
|
151
|
+
msal_client_id=msal_client_id,
|
|
152
|
+
msal_scopes=msal_scopes,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def save_profile(name: str, profile: DatavoProfile, *, set_active: bool = False) -> DatavoConfig:
|
|
157
|
+
config = load_config()
|
|
158
|
+
profiles = dict(config.profiles)
|
|
159
|
+
profiles[name] = profile
|
|
160
|
+
updated = DatavoConfig(
|
|
161
|
+
active_profile=name if set_active or config.active_profile is None else config.active_profile,
|
|
162
|
+
profiles=profiles,
|
|
163
|
+
)
|
|
164
|
+
save_config(updated)
|
|
165
|
+
return updated
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def update_profile(name: str, **changes) -> DatavoConfig:
|
|
169
|
+
config = load_config()
|
|
170
|
+
if name not in config.profiles:
|
|
171
|
+
raise RuntimeError(f"Unknown Datavo profile: {name}")
|
|
172
|
+
profile = replace(config.profiles[name], **changes)
|
|
173
|
+
profiles = dict(config.profiles)
|
|
174
|
+
profiles[name] = profile
|
|
175
|
+
updated = DatavoConfig(active_profile=config.active_profile, profiles=profiles)
|
|
176
|
+
save_config(updated)
|
|
177
|
+
return updated
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def set_active_profile(name: str) -> DatavoConfig:
|
|
181
|
+
config = load_config()
|
|
182
|
+
if name not in config.profiles:
|
|
183
|
+
raise RuntimeError(f"Unknown Datavo profile: {name}")
|
|
184
|
+
updated = DatavoConfig(active_profile=name, profiles=dict(config.profiles))
|
|
185
|
+
save_config(updated)
|
|
186
|
+
return updated
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def remove_profile(name: str) -> DatavoConfig:
|
|
190
|
+
config = load_config()
|
|
191
|
+
if name not in config.profiles:
|
|
192
|
+
raise RuntimeError(f"Unknown Datavo profile: {name}")
|
|
193
|
+
profiles = dict(config.profiles)
|
|
194
|
+
profiles.pop(name, None)
|
|
195
|
+
active_profile = config.active_profile
|
|
196
|
+
if active_profile == name:
|
|
197
|
+
active_profile = next(iter(sorted(profiles)), None)
|
|
198
|
+
updated = DatavoConfig(active_profile=active_profile, profiles=profiles)
|
|
199
|
+
save_config(updated)
|
|
200
|
+
return updated
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Version-skew diagnostics for the CLI error path.
|
|
2
|
+
|
|
3
|
+
A stale CLI meeting a newer server is the most likely thing to go wrong for a
|
|
4
|
+
user who installed once and came back later. When a command fails with a 422 or
|
|
5
|
+
a response the client cannot parse, the handler appends one actionable line
|
|
6
|
+
comparing the client's version to the server's ``release_version`` (fetched only
|
|
7
|
+
on the error path, so the happy path stays at one round trip).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.metadata
|
|
13
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
14
|
+
|
|
15
|
+
from datavo_sdk import fetch_server_config
|
|
16
|
+
|
|
17
|
+
_CLI_DISTRIBUTION = "datavo-cli"
|
|
18
|
+
_UPGRADE_HINT = "uv tool upgrade datavo-cli"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_client_version() -> str:
|
|
22
|
+
"""The CLI's real installed version.
|
|
23
|
+
|
|
24
|
+
Reads the published distribution version (injected at build time), not the
|
|
25
|
+
hard-coded ``datavo_cli.__version__`` placeholder — a skew line is only worth
|
|
26
|
+
printing if the client version is real. Falls back to ``__version__`` when the
|
|
27
|
+
distribution metadata is absent (e.g. running from a source checkout).
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
return importlib.metadata.version(_CLI_DISTRIBUTION)
|
|
31
|
+
except importlib.metadata.PackageNotFoundError:
|
|
32
|
+
from . import __version__
|
|
33
|
+
|
|
34
|
+
return __version__
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def server_root_from_request_url(url: str) -> str | None:
|
|
38
|
+
"""The server root (where ``/config`` lives) for a failing request URL.
|
|
39
|
+
|
|
40
|
+
Mirrors the transport: the API is mounted under ``/api``, while ``/config``
|
|
41
|
+
sits at the root. Returns ``None`` if the URL is not absolute.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
parts = urlsplit(url)
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
47
|
+
if not parts.scheme or not parts.netloc:
|
|
48
|
+
return None
|
|
49
|
+
idx = parts.path.find("/api")
|
|
50
|
+
base_path = parts.path[:idx] if idx != -1 else ""
|
|
51
|
+
return urlunsplit((parts.scheme, parts.netloc, base_path, "", ""))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def version_skew_line(
|
|
55
|
+
server_root: str,
|
|
56
|
+
*,
|
|
57
|
+
client_version: str | None = None,
|
|
58
|
+
timeout: float = 5.0,
|
|
59
|
+
) -> str | None:
|
|
60
|
+
"""One actionable upgrade line when client and server versions differ, else ``None``.
|
|
61
|
+
|
|
62
|
+
Best-effort and never raises: it fetches ``GET /config`` for the server's
|
|
63
|
+
``release_version`` and returns ``None`` when the versions match or the server
|
|
64
|
+
version cannot be determined, so no misleading upgrade line is printed.
|
|
65
|
+
"""
|
|
66
|
+
client = client_version or resolve_client_version()
|
|
67
|
+
try:
|
|
68
|
+
config = fetch_server_config(server_root, timeout=timeout)
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
server = getattr(config, "release_version", None)
|
|
72
|
+
if not server or server == client:
|
|
73
|
+
return None
|
|
74
|
+
return (
|
|
75
|
+
f"datavo CLI {client} is talking to server {server} — "
|
|
76
|
+
f"upgrade with `{_UPGRADE_HINT}`"
|
|
77
|
+
)
|
datavo_cli/errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Rendering CLI errors legibly, and appending a version-skew line when skew is the
|
|
2
|
+
likely cause. Kept out of ``main`` so the entrypoint stays pure dispatch.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from datavo_sdk import DatavoApiError, DatavoResponseParseError
|
|
12
|
+
|
|
13
|
+
from .diagnostics import server_root_from_request_url, version_skew_line
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def verbose_enabled(args) -> bool:
|
|
17
|
+
if getattr(args, "verbose", False):
|
|
18
|
+
return True
|
|
19
|
+
return os.environ.get("DATAVO_VERBOSE", "").strip().lower() in ("1", "true", "yes")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def report_cli_error(exc: Exception, *, verbose: bool) -> None:
|
|
23
|
+
"""Render a CLI error and, on skew-shaped failures, append a version line.
|
|
24
|
+
|
|
25
|
+
A ``DatavoApiError`` already composes method/url/status in its message (so we
|
|
26
|
+
print the whole message, not the bare detail); a 422 or a client-side
|
|
27
|
+
``DatavoResponseParseError`` is the case where a client/server version-skew line
|
|
28
|
+
is worth appending.
|
|
29
|
+
"""
|
|
30
|
+
skew_url: str | None = None
|
|
31
|
+
if isinstance(exc, DatavoApiError):
|
|
32
|
+
print(str(exc), file=sys.stderr)
|
|
33
|
+
if verbose and exc.raw_detail is not None:
|
|
34
|
+
print(json.dumps(exc.raw_detail, indent=2), file=sys.stderr)
|
|
35
|
+
if exc.status_code == 422:
|
|
36
|
+
skew_url = exc.url
|
|
37
|
+
elif isinstance(exc, DatavoResponseParseError):
|
|
38
|
+
print(str(exc), file=sys.stderr)
|
|
39
|
+
if verbose:
|
|
40
|
+
print(str(exc.error), file=sys.stderr)
|
|
41
|
+
skew_url = exc.url
|
|
42
|
+
else:
|
|
43
|
+
print(str(exc), file=sys.stderr)
|
|
44
|
+
|
|
45
|
+
if skew_url is not None:
|
|
46
|
+
root = server_root_from_request_url(skew_url)
|
|
47
|
+
line = version_skew_line(root) if root else None
|
|
48
|
+
if line:
|
|
49
|
+
print(line, file=sys.stderr)
|
datavo_cli/main.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import argcomplete
|
|
7
|
+
except ImportError: # pragma: no cover
|
|
8
|
+
argcomplete = None
|
|
9
|
+
|
|
10
|
+
from .errors import report_cli_error, verbose_enabled
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .commands import (
|
|
14
|
+
handle_auth_command,
|
|
15
|
+
handle_config_command,
|
|
16
|
+
handle_dataset_command,
|
|
17
|
+
handle_events_command,
|
|
18
|
+
handle_render_dataset_cache_command,
|
|
19
|
+
handle_sample_command,
|
|
20
|
+
handle_sample_store_command,
|
|
21
|
+
handle_source_import_command,
|
|
22
|
+
handle_split_set_command,
|
|
23
|
+
handle_share_command,
|
|
24
|
+
handle_stats_command,
|
|
25
|
+
handle_teams_command,
|
|
26
|
+
handle_transfer_command,
|
|
27
|
+
handle_unshare_command,
|
|
28
|
+
handle_users_command,
|
|
29
|
+
handle_warm_command,
|
|
30
|
+
)
|
|
31
|
+
from .parsers import build_root_parser
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
parser = build_root_parser()
|
|
36
|
+
if argcomplete is not None:
|
|
37
|
+
argcomplete.autocomplete(parser)
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
if args.command is None:
|
|
40
|
+
parser.print_help()
|
|
41
|
+
return 0
|
|
42
|
+
try:
|
|
43
|
+
if args.command == "config":
|
|
44
|
+
return handle_config_command(args)
|
|
45
|
+
if args.command == "auth":
|
|
46
|
+
return handle_auth_command(args)
|
|
47
|
+
if args.command == "sample-store":
|
|
48
|
+
return handle_sample_store_command(args)
|
|
49
|
+
if args.command == "source-import":
|
|
50
|
+
return handle_source_import_command(args)
|
|
51
|
+
if args.command == "sample":
|
|
52
|
+
return handle_sample_command(args)
|
|
53
|
+
if args.command == "stats":
|
|
54
|
+
return handle_stats_command(args)
|
|
55
|
+
if args.command == "dataset":
|
|
56
|
+
return handle_dataset_command(args)
|
|
57
|
+
if args.command == "split-set":
|
|
58
|
+
return handle_split_set_command(args)
|
|
59
|
+
if args.command == "teams":
|
|
60
|
+
return handle_teams_command(args)
|
|
61
|
+
if args.command == "share":
|
|
62
|
+
return handle_share_command(args)
|
|
63
|
+
if args.command == "unshare":
|
|
64
|
+
return handle_unshare_command(args)
|
|
65
|
+
if args.command == "transfer":
|
|
66
|
+
return handle_transfer_command(args)
|
|
67
|
+
if args.command == "users":
|
|
68
|
+
return handle_users_command(args)
|
|
69
|
+
if args.command == "events":
|
|
70
|
+
return handle_events_command(args)
|
|
71
|
+
if args.command == "render-dataset-cache":
|
|
72
|
+
return handle_render_dataset_cache_command(args)
|
|
73
|
+
if args.command == "warm":
|
|
74
|
+
return handle_warm_command(args)
|
|
75
|
+
if args.command == "version":
|
|
76
|
+
output_format = getattr(args, "format", "plain")
|
|
77
|
+
if output_format == "json":
|
|
78
|
+
print(f'{{"datavo_cli": "{__version__}"}}')
|
|
79
|
+
else:
|
|
80
|
+
print(__version__)
|
|
81
|
+
return 0
|
|
82
|
+
parser.error(f"Unknown command: {args.command}")
|
|
83
|
+
return 2
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
report_cli_error(exc, verbose=verbose_enabled(args))
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
raise SystemExit(main())
|