sv-cli 0.3.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 (49) hide show
  1. sv_cli/__init__.py +3 -0
  2. sv_cli/adapters.py +323 -0
  3. sv_cli/api_client.py +82 -0
  4. sv_cli/commands/__init__.py +0 -0
  5. sv_cli/commands/auth.py +4 -0
  6. sv_cli/commands/better_keywords.py +4 -0
  7. sv_cli/commands/call.py +4 -0
  8. sv_cli/commands/config.py +4 -0
  9. sv_cli/commands/content_quality.py +5 -0
  10. sv_cli/commands/content_transformer.py +4 -0
  11. sv_cli/commands/core_analysis.py +4 -0
  12. sv_cli/commands/definitions.py +4 -0
  13. sv_cli/commands/geo_audit.py +4 -0
  14. sv_cli/commands/insight_igniter.py +4 -0
  15. sv_cli/commands/marketplace_services.py +5 -0
  16. sv_cli/commands/options.py +4 -0
  17. sv_cli/commands/preliminary_audit.py +4 -0
  18. sv_cli/commands/ranklens.py +4 -0
  19. sv_cli/commands/seo_image.py +4 -0
  20. sv_cli/commands/seo_mapping.py +4 -0
  21. sv_cli/commands/seogpt.py +4 -0
  22. sv_cli/commands/seogpt2.py +4 -0
  23. sv_cli/commands/seogpt_compare.py +4 -0
  24. sv_cli/commands/top_competitors.py +5 -0
  25. sv_cli/commands/topical_authority.py +4 -0
  26. sv_cli/config.py +216 -0
  27. sv_cli/definitions.py +283 -0
  28. sv_cli/errors.py +55 -0
  29. sv_cli/executor.py +188 -0
  30. sv_cli/formatter.py +227 -0
  31. sv_cli/main.py +905 -0
  32. sv_cli/renderers/__init__.py +0 -0
  33. sv_cli/renderers/csv_renderer.py +9 -0
  34. sv_cli/renderers/json_renderer.py +10 -0
  35. sv_cli/renderers/markdown_renderer.py +9 -0
  36. sv_cli/renderers/table_renderer.py +9 -0
  37. sv_cli/renderers/text_renderer.py +9 -0
  38. sv_cli/resolver.py +523 -0
  39. sv_cli/schemas/__init__.py +0 -0
  40. sv_cli/schemas/api_response.py +12 -0
  41. sv_cli/schemas/config.py +13 -0
  42. sv_cli/schemas/tool_definition.py +17 -0
  43. sv_cli/tasks.py +168 -0
  44. sv_cli/utils.py +204 -0
  45. sv_cli-0.3.0.dist-info/METADATA +338 -0
  46. sv_cli-0.3.0.dist-info/RECORD +49 -0
  47. sv_cli-0.3.0.dist-info/WHEEL +4 -0
  48. sv_cli-0.3.0.dist-info/entry_points.txt +2 -0
  49. sv_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
sv_cli/config.py ADDED
@@ -0,0 +1,216 @@
1
+ """Configuration, profiles, and API-key resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .errors import AuthError, ConfigError
11
+ from .utils import app_dir, legacy_app_dir, mask_mapping, read_json_file, write_json_file
12
+
13
+ DEFAULT_BASE_URL = "https://ai.seovendor.co/api/"
14
+ DEFAULT_CACHE_TTL_SECONDS = 24 * 60 * 60
15
+
16
+
17
+ def config_path() -> Path:
18
+ return app_dir() / "config.json"
19
+
20
+
21
+ def legacy_config_path() -> Path:
22
+ return legacy_app_dir() / "config.json"
23
+
24
+
25
+ def default_config() -> dict[str, Any]:
26
+ return {
27
+ "default_profile": "default",
28
+ "profiles": {
29
+ "default": {
30
+ "api_key": None,
31
+ "base_url": DEFAULT_BASE_URL,
32
+ }
33
+ },
34
+ "defaults": {
35
+ "format": "pretty",
36
+ "cache_ttl_seconds": DEFAULT_CACHE_TTL_SECONDS,
37
+ "strict": False,
38
+ "fuzzy": True,
39
+ },
40
+ }
41
+
42
+
43
+ def load_config() -> dict[str, Any]:
44
+ path = config_path()
45
+ if not path.exists():
46
+ # When a caller explicitly sets SV_HOME or SEOVENDOR_HOME, keep all
47
+ # reads isolated to that directory. Otherwise, read the legacy default
48
+ # path once as a migration aid for pre-rename installs.
49
+ if os.environ.get("SV_HOME") or os.environ.get("SEOVENDOR_HOME"):
50
+ return default_config()
51
+ legacy_path = legacy_config_path()
52
+ if legacy_path.exists():
53
+ path = legacy_path
54
+ else:
55
+ return default_config()
56
+ try:
57
+ loaded = read_json_file(path, default_config())
58
+ except ValueError as exc:
59
+ raise ConfigError(str(exc)) from exc
60
+ cfg = default_config()
61
+ cfg.update({k: v for k, v in loaded.items() if k != "profiles"})
62
+ cfg["profiles"].update(loaded.get("profiles", {}))
63
+ cfg.setdefault("defaults", {}).update(loaded.get("defaults", {}))
64
+ if cfg.get("default_profile") not in cfg.get("profiles", {}):
65
+ cfg["default_profile"] = "default"
66
+ cfg["profiles"].setdefault("default", {"api_key": None, "base_url": DEFAULT_BASE_URL})
67
+ return cfg
68
+
69
+
70
+ def save_config(cfg: dict[str, Any]) -> None:
71
+ write_json_file(config_path(), cfg)
72
+
73
+
74
+ def masked_config() -> dict[str, Any]:
75
+ return mask_mapping(load_config())
76
+
77
+
78
+ def get_profile_name(cfg: dict[str, Any], profile: str | None = None) -> str:
79
+ return profile or cfg.get("default_profile") or "default"
80
+
81
+
82
+ def get_profile(cfg: dict[str, Any], profile: str | None = None, create: bool = False) -> dict[str, Any]:
83
+ name = get_profile_name(cfg, profile)
84
+ profiles = cfg.setdefault("profiles", {})
85
+ if name not in profiles:
86
+ if not create:
87
+ raise ConfigError(f'Profile "{name}" does not exist. Create it with: sv profile create {name}')
88
+ profiles[name] = {"api_key": None, "base_url": DEFAULT_BASE_URL}
89
+ profiles[name].setdefault("base_url", DEFAULT_BASE_URL)
90
+ profiles[name].setdefault("api_key", None)
91
+ return profiles[name]
92
+
93
+
94
+ def resolve_base_url(cli_base_url: str | None = None, profile: str | None = None) -> str:
95
+ if cli_base_url:
96
+ return normalize_base_url(cli_base_url)
97
+ cfg = load_config()
98
+ value = get_profile(cfg, profile).get("base_url") or DEFAULT_BASE_URL
99
+ return normalize_base_url(value)
100
+
101
+
102
+ def normalize_base_url(base_url: str) -> str:
103
+ return base_url.rstrip("/") + "/"
104
+
105
+
106
+ def resolve_api_key(
107
+ *,
108
+ cli_api_key: str | None = None,
109
+ profile: str | None = None,
110
+ allow_prompt: bool = False,
111
+ non_interactive: bool = False,
112
+ ) -> str:
113
+ if cli_api_key:
114
+ return cli_api_key
115
+ env_key = os.environ.get("SV_API_KEY") or os.environ.get("SEOVENDOR_API_KEY")
116
+ if env_key:
117
+ return env_key
118
+ cfg = load_config()
119
+ stored = get_profile(cfg, profile).get("api_key")
120
+ if stored:
121
+ return stored
122
+ if allow_prompt and not non_interactive:
123
+ entered = getpass.getpass("SV API key: ").strip()
124
+ if entered:
125
+ return entered
126
+ raise AuthError(
127
+ "Missing SV API key. Set SV_API_KEY, run `sv auth set`, "
128
+ "or pass --api-key. SEOVENDOR_API_KEY is also accepted as a legacy fallback. "
129
+ "Avoid --api-key in shared shells because it may be saved in history."
130
+ )
131
+
132
+
133
+ def set_api_key(api_key: str, profile: str | None = None) -> None:
134
+ cfg = load_config()
135
+ prof = get_profile(cfg, profile, create=True)
136
+ prof["api_key"] = api_key
137
+ save_config(cfg)
138
+
139
+
140
+ def clear_api_key(profile: str | None = None) -> None:
141
+ cfg = load_config()
142
+ prof = get_profile(cfg, profile, create=True)
143
+ prof["api_key"] = None
144
+ save_config(cfg)
145
+
146
+
147
+ def set_profile_value(profile: str, key: str, value: Any) -> None:
148
+ cfg = load_config()
149
+ prof = get_profile(cfg, profile, create=True)
150
+ prof[key] = value
151
+ save_config(cfg)
152
+
153
+
154
+ def create_profile(name: str, *, base_url: str | None = None, api_key: str | None = None) -> None:
155
+ cfg = load_config()
156
+ if name in cfg.setdefault("profiles", {}):
157
+ raise ConfigError(f'Profile "{name}" already exists.')
158
+ cfg["profiles"][name] = {
159
+ "api_key": api_key,
160
+ "base_url": normalize_base_url(base_url or DEFAULT_BASE_URL),
161
+ }
162
+ save_config(cfg)
163
+
164
+
165
+ def delete_profile(name: str) -> None:
166
+ cfg = load_config()
167
+ if name == "default":
168
+ raise ConfigError('The "default" profile cannot be deleted.')
169
+ if name not in cfg.setdefault("profiles", {}):
170
+ raise ConfigError(f'Profile "{name}" does not exist.')
171
+ del cfg["profiles"][name]
172
+ if cfg.get("default_profile") == name:
173
+ cfg["default_profile"] = "default"
174
+ save_config(cfg)
175
+
176
+
177
+ def use_profile(name: str) -> None:
178
+ cfg = load_config()
179
+ if name not in cfg.setdefault("profiles", {}):
180
+ raise ConfigError(f'Profile "{name}" does not exist. Create it with: sv profile create {name}')
181
+ cfg["default_profile"] = name
182
+ save_config(cfg)
183
+
184
+
185
+ def set_config_value(key: str, value: Any, profile: str | None = None) -> None:
186
+ cfg = load_config()
187
+ if key == "base_url":
188
+ get_profile(cfg, profile, create=True)[key] = normalize_base_url(str(value))
189
+ elif key == "api_key":
190
+ get_profile(cfg, profile, create=True)[key] = str(value)
191
+ elif key == "default_profile":
192
+ if value not in cfg.get("profiles", {}):
193
+ raise ConfigError(f'Profile "{value}" does not exist.')
194
+ cfg["default_profile"] = str(value)
195
+ else:
196
+ cfg.setdefault("defaults", {})[key] = value
197
+ save_config(cfg)
198
+
199
+
200
+ def get_config_value(key: str, profile: str | None = None) -> Any:
201
+ cfg = load_config()
202
+ if key in {"base_url", "api_key"}:
203
+ value = get_profile(cfg, profile).get(key)
204
+ elif key == "default_profile":
205
+ value = cfg.get("default_profile")
206
+ else:
207
+ value = cfg.setdefault("defaults", {}).get(key)
208
+ if key == "api_key" and value:
209
+ return "***masked***" + str(value)[-4:]
210
+ return value
211
+
212
+
213
+ def reset_config() -> None:
214
+ path = config_path()
215
+ if path.exists():
216
+ path.unlink()
sv_cli/definitions.py ADDED
@@ -0,0 +1,283 @@
1
+ """Dynamic SV API discovery and local definitions cache."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from urllib.parse import urljoin
11
+
12
+ import httpx
13
+
14
+ from .adapters import TOOL_ADAPTERS, all_command_names, tool_display_name
15
+ from .config import DEFAULT_BASE_URL, load_config, normalize_base_url
16
+ from .errors import ConfigError, NetworkError
17
+ from .utils import app_dir, normalize_text, read_json_file, write_json_file
18
+
19
+ CACHE_VERSION = 1
20
+
21
+
22
+ def cache_path() -> Path:
23
+ return app_dir() / "cache" / "definitions.json"
24
+
25
+
26
+ def utc_now() -> str:
27
+ return dt.datetime.now(dt.timezone.utc).isoformat()
28
+
29
+
30
+ def parse_timestamp(value: str | None) -> float:
31
+ if not value:
32
+ return 0.0
33
+ try:
34
+ return dt.datetime.fromisoformat(value).timestamp()
35
+ except ValueError:
36
+ return 0.0
37
+
38
+
39
+ def default_cache(base_url: str = DEFAULT_BASE_URL) -> dict[str, Any]:
40
+ return {
41
+ "version": CACHE_VERSION,
42
+ "base_url": normalize_base_url(base_url),
43
+ "fetched_at": None,
44
+ "root": {},
45
+ "tools": {},
46
+ "warnings": [],
47
+ }
48
+
49
+
50
+ class DefinitionsManager:
51
+ """Fetch, cache, and resolve API root/tool definitions."""
52
+
53
+ def __init__(self, base_url: str | None = None, *, ttl_seconds: int | None = None) -> None:
54
+ cfg = load_config()
55
+ profile_name = cfg.get("default_profile") or "default"
56
+ profile = cfg.get("profiles", {}).get(profile_name, {})
57
+ self.base_url = normalize_base_url(base_url or profile.get("base_url") or DEFAULT_BASE_URL)
58
+ self.ttl_seconds = int(
59
+ ttl_seconds
60
+ if ttl_seconds is not None
61
+ else cfg.get("defaults", {}).get("cache_ttl_seconds", 24 * 60 * 60)
62
+ )
63
+ self.path = cache_path()
64
+
65
+ def load_cache(self) -> dict[str, Any]:
66
+ try:
67
+ cached = read_json_file(self.path, default_cache(self.base_url))
68
+ except ValueError as exc:
69
+ raise ConfigError(str(exc)) from exc
70
+ if not cached:
71
+ return default_cache(self.base_url)
72
+ cached.setdefault("version", CACHE_VERSION)
73
+ cached.setdefault("base_url", self.base_url)
74
+ cached.setdefault("root", {})
75
+ cached.setdefault("tools", {})
76
+ cached.setdefault("warnings", [])
77
+ self.ensure_supplemental_tools(cached)
78
+ return cached
79
+
80
+ def save_cache(self, cache: dict[str, Any]) -> None:
81
+ cache["version"] = CACHE_VERSION
82
+ cache["base_url"] = self.base_url
83
+ write_json_file(self.path, cache)
84
+
85
+ def clear_cache(self) -> bool:
86
+ if self.path.exists():
87
+ self.path.unlink()
88
+ return True
89
+ return False
90
+
91
+ def is_stale(self, cache: dict[str, Any]) -> bool:
92
+ if not cache.get("root"):
93
+ return True
94
+ fetched = parse_timestamp(cache.get("fetched_at"))
95
+ return (time.time() - fetched) > self.ttl_seconds
96
+
97
+ def get_cache(self, *, refresh: bool = False, allow_stale: bool = True) -> dict[str, Any]:
98
+ cache = self.load_cache()
99
+ if refresh or self.is_stale(cache):
100
+ try:
101
+ return self.refresh_all()
102
+ except Exception as exc: # noqa: BLE001 - cache fallback should be broad
103
+ if allow_stale and cache.get("root"):
104
+ cache.setdefault("warnings", []).append(
105
+ f"Definitions refresh failed; using stale cache: {exc}"
106
+ )
107
+ return cache
108
+ if isinstance(exc, NetworkError):
109
+ raise
110
+ raise NetworkError(f"Could not fetch API definitions and no usable cache exists: {exc}") from exc
111
+ return cache
112
+
113
+ def fetch_json(self, url: str) -> Any:
114
+ try:
115
+ with httpx.Client(timeout=30.0, follow_redirects=True) as client:
116
+ response = client.get(url)
117
+ response.raise_for_status()
118
+ return response.json()
119
+ except httpx.TimeoutException as exc:
120
+ raise NetworkError(f"Timed out while fetching {url}") from exc
121
+ except httpx.HTTPStatusError as exc:
122
+ raise NetworkError(
123
+ f"Definitions request failed for {url}: HTTP {exc.response.status_code}"
124
+ ) from exc
125
+ except httpx.RequestError as exc:
126
+ raise NetworkError(f"Network error while fetching {url}: {exc}") from exc
127
+ except json.JSONDecodeError as exc:
128
+ raise NetworkError(f"Definitions endpoint did not return valid JSON: {url}") from exc
129
+
130
+ def supplemental_root_entries(self) -> dict[str, dict[str, str]]:
131
+ """Return adapter-provided endpoint hints for tools missing from the API root.
132
+
133
+ These entries are a compatibility bridge for newly released tools whose
134
+ definitions endpoints are live before the API root advertises them.
135
+ They are deliberately derived from the active base URL, so profiles and
136
+ staging URLs still work.
137
+ """
138
+
139
+ entries: dict[str, dict[str, str]] = {}
140
+ for canonical, adapter in TOOL_ADAPTERS.items():
141
+ if not adapter.endpoint_path:
142
+ continue
143
+ definitions_path = adapter.definitions_path or f"{adapter.endpoint_path.rstrip('/')}/definitions"
144
+ entries[canonical] = {
145
+ "Endpoint": join_base_url(self.base_url, adapter.endpoint_path),
146
+ "Definitions": join_base_url(self.base_url, definitions_path),
147
+ "Source": "local-adapter-fallback",
148
+ }
149
+ return entries
150
+
151
+ def ensure_supplemental_tools(self, cache: dict[str, Any]) -> None:
152
+ """Merge local adapter hints into a cache without overwriting live root data."""
153
+
154
+ root = cache.setdefault("root", {})
155
+ tools = cache.setdefault("tools", {})
156
+ for tool_name, meta in self.supplemental_root_entries().items():
157
+ if tool_name in root:
158
+ continue
159
+ root[tool_name] = meta
160
+ tools.setdefault(
161
+ tool_name,
162
+ {
163
+ "tool": tool_name,
164
+ "display_name": tool_display_name(tool_name),
165
+ "endpoint": meta.get("Endpoint"),
166
+ "definitions_url": meta.get("Definitions"),
167
+ "definition": None,
168
+ "last_fetched": None,
169
+ "source": meta.get("Source"),
170
+ },
171
+ )
172
+
173
+ def refresh_all(self) -> dict[str, Any]:
174
+ root = self.fetch_json(self.base_url)
175
+ if not isinstance(root, dict):
176
+ raise NetworkError(f"API root {self.base_url} did not return a JSON object")
177
+ cache = default_cache(self.base_url)
178
+ cache["root"] = dict(root)
179
+ self.ensure_supplemental_tools(cache)
180
+ cache["fetched_at"] = utc_now()
181
+ warnings: list[str] = []
182
+ for tool_name, meta in cache["root"].items():
183
+ endpoint = _pick_value(meta, "Endpoint", "endpoint", "url")
184
+ definitions_url = _pick_value(meta, "Definitions", "definitions", "definition", "schema")
185
+ existing_entry = cache.get("tools", {}).get(tool_name, {})
186
+ tool_entry = {
187
+ "tool": tool_name,
188
+ "display_name": tool_display_name(tool_name),
189
+ "endpoint": endpoint or existing_entry.get("endpoint"),
190
+ "definitions_url": definitions_url or existing_entry.get("definitions_url"),
191
+ "definition": existing_entry.get("definition"),
192
+ "last_fetched": existing_entry.get("last_fetched"),
193
+ "source": existing_entry.get("source", "api-root"),
194
+ }
195
+ if tool_entry.get("definitions_url"):
196
+ try:
197
+ tool_entry["definition"] = self.fetch_json(str(tool_entry["definitions_url"]))
198
+ tool_entry["last_fetched"] = utc_now()
199
+ except Exception as exc: # noqa: BLE001 - partial cache is still useful
200
+ warnings.append(f"Could not fetch definitions for {tool_name}: {exc}")
201
+ cache["tools"][tool_name] = tool_entry
202
+ cache["warnings"] = warnings
203
+ self.save_cache(cache)
204
+ return cache
205
+
206
+ def refresh_tool(self, tool_name: str) -> dict[str, Any]:
207
+ cache = self.get_cache(allow_stale=True)
208
+ canonical = self.resolve_tool(tool_name, cache)
209
+ entry = cache.get("tools", {}).get(canonical)
210
+ if not entry:
211
+ raise ConfigError(f'Tool "{tool_name}" is not available in the API root.')
212
+ definitions_url = entry.get("definitions_url")
213
+ if not definitions_url:
214
+ raise NetworkError(f'Tool "{canonical}" does not expose a definitions URL in the root.')
215
+ entry["definition"] = self.fetch_json(str(definitions_url))
216
+ entry["last_fetched"] = utc_now()
217
+ cache["tools"][canonical] = entry
218
+ self.save_cache(cache)
219
+ return entry
220
+
221
+ def resolve_tool(self, user_value: str, cache: dict[str, Any] | None = None) -> str:
222
+ cache = cache or self.get_cache(allow_stale=True)
223
+ root_tools = set(cache.get("root", {}).keys()) | set(cache.get("tools", {}).keys())
224
+ if user_value in root_tools:
225
+ return user_value
226
+ alias_map = all_command_names()
227
+ if user_value in alias_map:
228
+ preferred = alias_map[user_value]
229
+ if preferred in root_tools:
230
+ return preferred
231
+ norm_user = normalize_text(user_value).replace(" ", "")
232
+ for name in root_tools:
233
+ if normalize_text(name).replace(" ", "") == norm_user:
234
+ return name
235
+ for alias, canonical in alias_map.items():
236
+ if normalize_text(alias).replace(" ", "") == norm_user and canonical in root_tools:
237
+ return canonical
238
+ known = ", ".join(sorted(root_tools)) or "none"
239
+ raise ConfigError(f'Unknown tool "{user_value}". Available tools: {known}')
240
+
241
+ def get_tool(self, user_value: str, *, refresh: bool = False) -> dict[str, Any]:
242
+ cache = self.get_cache(refresh=refresh, allow_stale=True)
243
+ canonical = self.resolve_tool(user_value, cache)
244
+ entry = cache.get("tools", {}).get(canonical)
245
+ if entry:
246
+ if entry.get("definition") is None and entry.get("definitions_url"):
247
+ try:
248
+ entry["definition"] = self.fetch_json(str(entry["definitions_url"]))
249
+ entry["last_fetched"] = utc_now()
250
+ cache.setdefault("tools", {})[canonical] = entry
251
+ self.save_cache(cache)
252
+ except Exception as exc: # noqa: BLE001 - callers can still use raw or schema-light tools
253
+ entry["definition_warning"] = f"Could not fetch definitions: {exc}"
254
+ return entry
255
+ meta = cache.get("root", {}).get(canonical, {})
256
+ entry = {
257
+ "tool": canonical,
258
+ "display_name": tool_display_name(canonical),
259
+ "endpoint": _pick_value(meta, "Endpoint", "endpoint", "url"),
260
+ "definitions_url": _pick_value(meta, "Definitions", "definitions", "definition", "schema"),
261
+ "definition": None,
262
+ "last_fetched": None,
263
+ }
264
+ cache.setdefault("tools", {})[canonical] = entry
265
+ self.save_cache(cache)
266
+ return entry
267
+
268
+
269
+ def join_base_url(base_url: str, path: str) -> str:
270
+ return urljoin(normalize_base_url(base_url), path.lstrip("/"))
271
+
272
+
273
+ def _pick_value(data: Any, *keys: str) -> Any:
274
+ if not isinstance(data, dict):
275
+ return None
276
+ for key in keys:
277
+ if key in data:
278
+ return data[key]
279
+ lowered = {str(k).lower(): v for k, v in data.items()}
280
+ for key in keys:
281
+ if key.lower() in lowered:
282
+ return lowered[key.lower()]
283
+ return None
sv_cli/errors.py ADDED
@@ -0,0 +1,55 @@
1
+ """Typed exceptions and exit-code helpers for SV CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class CLIError(Exception):
10
+ """Base error carrying a process exit code."""
11
+
12
+ message: str
13
+ exit_code: int = 1
14
+
15
+ def __str__(self) -> str: # pragma: no cover - trivial
16
+ return self.message
17
+
18
+
19
+ class InvalidInputError(CLIError):
20
+ def __init__(self, message: str) -> None:
21
+ super().__init__(message, 2)
22
+
23
+
24
+ class AmbiguousMatchError(InvalidInputError):
25
+ pass
26
+
27
+
28
+ class AuthError(CLIError):
29
+ def __init__(self, message: str) -> None:
30
+ super().__init__(message, 3)
31
+
32
+
33
+ class APIError(CLIError):
34
+ def __init__(self, message: str) -> None:
35
+ super().__init__(message, 4)
36
+
37
+
38
+ class NetworkError(CLIError):
39
+ def __init__(self, message: str) -> None:
40
+ super().__init__(message, 5)
41
+
42
+
43
+ class TimeoutError(CLIError):
44
+ def __init__(self, message: str) -> None:
45
+ super().__init__(message, 6)
46
+
47
+
48
+ class ConfigError(CLIError):
49
+ def __init__(self, message: str) -> None:
50
+ super().__init__(message, 7)
51
+
52
+
53
+ class UnsupportedFeatureError(CLIError):
54
+ def __init__(self, message: str) -> None:
55
+ super().__init__(message, 8)