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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """SV CLI package."""
2
+
3
+ __version__ = "0.3.0"
sv_cli/adapters.py ADDED
@@ -0,0 +1,323 @@
1
+ """Local command adapters.
2
+
3
+ Adapters intentionally contain only human-friendly command metadata. Endpoints,
4
+ required fields, and enums are still discovered from SV API definitions
5
+ at runtime whenever possible.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ToolAdapter:
16
+ canonical: str
17
+ command: str
18
+ aliases: tuple[str, ...] = ()
19
+ default_action: str = "run"
20
+ actions: tuple[str, ...] = ("run",)
21
+ field_aliases: dict[str, tuple[str, ...]] = field(default_factory=dict)
22
+ option_aliases: dict[str, tuple[str, ...]] = field(default_factory=dict)
23
+ async_likely: bool = False
24
+ # Optional local discovery hints. These are used only when the live API root
25
+ # has not yet exposed a known tool. The API root remains the source of truth
26
+ # and overrides these values whenever it contains the tool.
27
+ endpoint_path: str | None = None
28
+ definitions_path: str | None = None
29
+
30
+
31
+ COMMON_FIELD_ALIASES: dict[str, tuple[str, ...]] = {
32
+ "keyword": ("keyword", "kw", "query", "seed_keyword"),
33
+ "keywords": ("keywords", "kws", "keyword_list", "kwlist"),
34
+ "url": ("url", "domain", "page_url", "website"),
35
+ "url_a": ("url_a", "urla", "url1", "first_url", "competitor_url"),
36
+ "url_b": ("url_b", "urlb", "url2", "second_url", "comparison_url"),
37
+ "brand": ("brand", "brand_name", "company", "company_name"),
38
+ "type": ("contenttype", "content_type", "imagetype", "image_type", "type"),
39
+ "language": ("language", "lang", "language_id"),
40
+ "engine": ("engine", "ai_engine", "model"),
41
+ "country": ("country", "country_code", "location", "gl"),
42
+ "location": ("location", "geo", "country", "market"),
43
+ "text": ("text", "content", "input", "body"),
44
+ "search": ("searchterm", "search_term", "search", "query", "term"),
45
+ "query": ("searchterm", "search_term", "query", "search", "term"),
46
+ "price": ("price", "max_price", "price_ceiling"),
47
+ "series": ("series", "service_series"),
48
+ "category": ("category", "service_category"),
49
+ "outline": ("outline", "outline_text"),
50
+ "theme": ("theme", "image_theme"),
51
+ "background": ("background", "bg", "image_background"),
52
+ "color": ("color", "colour", "palette"),
53
+ "size": ("size", "image_size", "dimensions"),
54
+ }
55
+
56
+ TOOL_ADAPTERS: dict[str, ToolAdapter] = {
57
+ "better-keywords": ToolAdapter(
58
+ canonical="better-keywords",
59
+ command="better-keywords",
60
+ aliases=("keywords",),
61
+ default_action="research",
62
+ actions=("research", "filter", "raw"),
63
+ field_aliases=COMMON_FIELD_ALIASES,
64
+ option_aliases={
65
+ "types": ("researchtype", "research_type", "type", "types"),
66
+ "languages": ("lang", "language", "languages"),
67
+ "competition": ("kwcompetition", "kw_competition", "competition"),
68
+ },
69
+ ),
70
+ "content-transformer": ToolAdapter(
71
+ canonical="content-transformer",
72
+ command="content-transformer",
73
+ aliases=("transform",),
74
+ default_action="rewrite",
75
+ actions=("rewrite", "raw"),
76
+ field_aliases=COMMON_FIELD_ALIASES,
77
+ option_aliases={
78
+ "types": ("type", "types", "contenttype", "content_type"),
79
+ "lengths": ("length", "lengths"),
80
+ "languages": ("lang", "language", "languages"),
81
+ "engines": ("engine", "model", "engines"),
82
+ },
83
+ ),
84
+ "core-analysis": ToolAdapter(
85
+ canonical="core-analysis",
86
+ command="core-analysis",
87
+ aliases=("core",),
88
+ default_action="analyze",
89
+ actions=("analyze", "raw"),
90
+ field_aliases=COMMON_FIELD_ALIASES,
91
+ ),
92
+ "geogptaudit": ToolAdapter(
93
+ canonical="geogptaudit",
94
+ command="geo-audit",
95
+ aliases=("geogpt-audit", "audit"),
96
+ default_action="create-task",
97
+ actions=("create-task", "get-task-status", "get-result", "raw"),
98
+ field_aliases=COMMON_FIELD_ALIASES,
99
+ async_likely=True,
100
+ option_aliases={
101
+ "types": ("scantype", "scan_type", "type", "types"),
102
+ "languages": ("lang", "language", "languages"),
103
+ },
104
+ ),
105
+ "insight-igniter": ToolAdapter(
106
+ canonical="insight-igniter",
107
+ command="insight-igniter",
108
+ aliases=("insights",),
109
+ default_action="entities",
110
+ actions=("entities", "raw"),
111
+ field_aliases=COMMON_FIELD_ALIASES,
112
+ option_aliases={
113
+ "languages": ("lang", "language", "languages"),
114
+ },
115
+ ),
116
+ "preliminaryaudit": ToolAdapter(
117
+ canonical="preliminaryaudit",
118
+ command="preliminary-audit",
119
+ aliases=("prelim-audit",),
120
+ default_action="analyze",
121
+ actions=("analyze", "raw"),
122
+ field_aliases=COMMON_FIELD_ALIASES,
123
+ ),
124
+ "ranklens": ToolAdapter(
125
+ canonical="ranklens",
126
+ command="ranklens",
127
+ aliases=(),
128
+ default_action="rank",
129
+ actions=("rank", "competitors", "raw"),
130
+ field_aliases=COMMON_FIELD_ALIASES,
131
+ option_aliases={
132
+ "languages": ("lang", "language", "languages"),
133
+ "engines": ("engine", "model", "engines"),
134
+ "sample-sizes": ("samplesize", "sample_size", "samplesize"),
135
+ },
136
+ ),
137
+ "seo-image": ToolAdapter(
138
+ canonical="seo-image",
139
+ command="seo-image",
140
+ aliases=("image",),
141
+ default_action="generate",
142
+ actions=("generate", "raw"),
143
+ field_aliases=COMMON_FIELD_ALIASES,
144
+ option_aliases={
145
+ "types": ("imagetype", "image_type", "type", "types"),
146
+ "themes": ("imagetheme", "image_theme", "theme", "themes"),
147
+ "backgrounds": ("background", "imagebackground", "backgrounds"),
148
+ "colors": ("primarycolor", "primary_color", "color", "colors"),
149
+ "secondary-colors": ("secondarycolor", "secondary_color", "secondarycolor"),
150
+ "sizes": ("imagesize", "image_size", "size", "sizes"),
151
+ "languages": ("lang", "language", "languages"),
152
+ "engines": ("engine", "model", "engines"),
153
+ },
154
+ ),
155
+ "seogpt": ToolAdapter(
156
+ canonical="seogpt",
157
+ command="seogpt",
158
+ aliases=("seo-gpt",),
159
+ default_action="generate",
160
+ actions=("generate", "raw"),
161
+ field_aliases={
162
+ **COMMON_FIELD_ALIASES,
163
+ "contenttype": ("type", "contenttype", "content_type"),
164
+ },
165
+ option_aliases={
166
+ "types": ("type", "types", "contenttype", "content_type"),
167
+ "languages": ("lang", "language", "languages"),
168
+ "engines": ("engine", "model", "engines"),
169
+ },
170
+ ),
171
+ "seogpt2": ToolAdapter(
172
+ canonical="seogpt2",
173
+ command="seogpt2",
174
+ aliases=("seo-gpt2",),
175
+ default_action="create-task",
176
+ actions=("create-task", "get-task-status", "get-result", "raw"),
177
+ field_aliases={
178
+ **COMMON_FIELD_ALIASES,
179
+ "keyword": ("Topic", "topic", "kw", "keyword", "query", "seed_keyword"),
180
+ "text": ("Topic", "topic", "text", "content", "input", "body"),
181
+ },
182
+ async_likely=True,
183
+ option_aliases={
184
+ "types": ("contenttype", "content_type", "type", "types"),
185
+ "lengths": ("contentlength", "content_length", "length", "lengths"),
186
+ "languages": ("language", "lang", "languages"),
187
+ "tones": ("tone", "tones"),
188
+ "persons": ("person", "persons"),
189
+ "writers": ("writer", "writers"),
190
+ "engines": ("engine", "model", "engines"),
191
+ "pathways": ("pathway", "pathways"),
192
+ },
193
+ ),
194
+ "seogptcompare": ToolAdapter(
195
+ canonical="seogptcompare",
196
+ command="seogpt-compare",
197
+ aliases=("compare",),
198
+ default_action="create-task",
199
+ actions=("create-task", "get-task-status", "get-result", "raw"),
200
+ field_aliases=COMMON_FIELD_ALIASES,
201
+ async_likely=True,
202
+ option_aliases={
203
+ "languages": ("lang", "language", "languages"),
204
+ "countries": ("country", "countries"),
205
+ },
206
+ ),
207
+ "seogptmapping": ToolAdapter(
208
+ canonical="seogptmapping",
209
+ command="seo-mapping",
210
+ aliases=("mapping",),
211
+ default_action="create-task",
212
+ actions=("create-task", "get-task-status", "get-result", "raw"),
213
+ field_aliases=COMMON_FIELD_ALIASES,
214
+ async_likely=True,
215
+ option_aliases={
216
+ "types": ("scantype", "scan_type", "type", "types"),
217
+ "languages": ("lang", "language", "languages"),
218
+ },
219
+ ),
220
+ "topical-authority": ToolAdapter(
221
+ canonical="topical-authority",
222
+ command="topical-authority",
223
+ aliases=("topical",),
224
+ default_action="topics",
225
+ actions=("topics", "content", "raw"),
226
+ field_aliases=COMMON_FIELD_ALIASES,
227
+ option_aliases={
228
+ "modes": ("topicmode", "topic_mode", "mode", "modes"),
229
+ "languages": ("lang", "language", "languages"),
230
+ "sizes": ("topicsize", "topic_size", "size", "sizes"),
231
+ },
232
+ ),
233
+ "top-competitors": ToolAdapter(
234
+ canonical="top-competitors",
235
+ command="top-competitors",
236
+ aliases=("competitors",),
237
+ default_action="analyze",
238
+ actions=("analyze", "raw"),
239
+ field_aliases={
240
+ **COMMON_FIELD_ALIASES,
241
+ "keyword": ("kw", "keyword", "query", "seed_keyword"),
242
+ },
243
+ endpoint_path="top-competitors/",
244
+ definitions_path="top-competitors/definitions",
245
+ ),
246
+ "marketplace-services": ToolAdapter(
247
+ canonical="marketplace-services",
248
+ command="marketplace-services",
249
+ aliases=("marketplace", "services"),
250
+ default_action="search",
251
+ actions=("search", "raw"),
252
+ field_aliases={
253
+ **COMMON_FIELD_ALIASES,
254
+ "keyword": ("searchterm", "search_term", "kw", "keyword", "query"),
255
+ "search": ("searchterm", "search_term", "search", "query", "term"),
256
+ "query": ("searchterm", "search_term", "query", "search", "term"),
257
+ },
258
+ option_aliases={
259
+ "series": ("series", "service_series"),
260
+ "categories": ("category", "service_category", "categories"),
261
+ },
262
+ endpoint_path="marketplace-services/",
263
+ definitions_path="marketplace-services/definitions",
264
+ ),
265
+ "content-quality": ToolAdapter(
266
+ canonical="content-quality",
267
+ command="content-quality",
268
+ aliases=("quality", "hcu-quality", "eeat-quality"),
269
+ default_action="analyze",
270
+ actions=("analyze", "raw"),
271
+ field_aliases={
272
+ **COMMON_FIELD_ALIASES,
273
+ "keyword": ("kw", "keyword", "query", "seed_keyword"),
274
+ "url": ("url1", "url", "domain", "page_url", "website"),
275
+ "url_a": ("url1", "url_a", "urla", "first_url"),
276
+ "url_b": ("url2", "url_b", "urlb", "second_url", "comparison_url"),
277
+ },
278
+ endpoint_path="content-quality/",
279
+ definitions_path="content-quality/definitions",
280
+ ),
281
+ }
282
+
283
+
284
+
285
+ def all_command_names() -> dict[str, str]:
286
+ """Return command/alias -> adapter canonical mapping."""
287
+
288
+ names: dict[str, str] = {}
289
+ for canonical, adapter in TOOL_ADAPTERS.items():
290
+ names[adapter.command] = canonical
291
+ names[canonical] = canonical
292
+ for alias in adapter.aliases:
293
+ names[alias] = canonical
294
+ return names
295
+
296
+
297
+ def get_adapter(tool: str) -> ToolAdapter | None:
298
+ return TOOL_ADAPTERS.get(tool)
299
+
300
+
301
+ def field_candidates(tool: str, cli_field: str) -> tuple[str, ...]:
302
+ adapter = TOOL_ADAPTERS.get(tool)
303
+ if adapter and cli_field in adapter.field_aliases:
304
+ return adapter.field_aliases[cli_field]
305
+ return COMMON_FIELD_ALIASES.get(cli_field, (cli_field,))
306
+
307
+
308
+ def tool_display_name(canonical: str) -> str:
309
+ adapter = TOOL_ADAPTERS.get(canonical)
310
+ return adapter.command if adapter else canonical
311
+
312
+
313
+ def adapter_as_dict(adapter: ToolAdapter) -> dict[str, Any]:
314
+ return {
315
+ "canonical": adapter.canonical,
316
+ "command": adapter.command,
317
+ "aliases": list(adapter.aliases),
318
+ "default_action": adapter.default_action,
319
+ "actions": list(adapter.actions),
320
+ "async_likely": adapter.async_likely,
321
+ "endpoint_path": adapter.endpoint_path,
322
+ "definitions_path": adapter.definitions_path,
323
+ }
sv_cli/api_client.py ADDED
@@ -0,0 +1,82 @@
1
+ """HTTP client for SV API calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+ from urllib.parse import urlparse
10
+
11
+ import httpx
12
+ from rich.console import Console
13
+
14
+ from .errors import APIError, NetworkError, TimeoutError
15
+ from .utils import mask_mapping
16
+
17
+
18
+ @dataclass
19
+ class APIResponse:
20
+ data: Any
21
+ status_code: int
22
+ elapsed_seconds: float
23
+ url: str
24
+
25
+
26
+ class APIClient:
27
+ def __init__(self, *, debug: bool = False, console: Console | None = None) -> None:
28
+ self.debug = debug
29
+ self.console = console or Console(stderr=True)
30
+
31
+ def request_tool(
32
+ self,
33
+ *,
34
+ endpoint: str,
35
+ payload: dict[str, Any],
36
+ api_key: str | None,
37
+ method: str = "POST",
38
+ timeout: float = 60.0,
39
+ ) -> APIResponse:
40
+ final_payload = dict(payload)
41
+ if api_key and not any(key in final_payload for key in ("k", "api_key", "apikey", "key")):
42
+ final_payload["k"] = api_key
43
+
44
+ method = method.upper()
45
+ if self.debug:
46
+ parsed = urlparse(endpoint)
47
+ shown_path = parsed.path or endpoint
48
+ self.console.print(f"[dim]{method} {shown_path}[/dim]")
49
+ self.console.print_json(json.dumps(mask_mapping(final_payload)))
50
+
51
+ started = time.perf_counter()
52
+ try:
53
+ with httpx.Client(timeout=timeout, follow_redirects=True) as client:
54
+ if method == "GET":
55
+ response = client.get(endpoint, params=final_payload)
56
+ elif method in {"POST", "PUT", "PATCH"}:
57
+ response = client.request(method, endpoint, json=final_payload)
58
+ else:
59
+ raise APIError(f"Unsupported HTTP method: {method}")
60
+ except httpx.TimeoutException as exc:
61
+ raise TimeoutError(f"Request timed out after {timeout:g} seconds.") from exc
62
+ except httpx.RequestError as exc:
63
+ raise NetworkError(f"Network error while calling SV API: {exc}") from exc
64
+
65
+ elapsed = time.perf_counter() - started
66
+ if self.debug:
67
+ self.console.print(f"[dim]HTTP {response.status_code} in {elapsed:.2f}s[/dim]")
68
+
69
+ try:
70
+ data = response.json()
71
+ except json.JSONDecodeError:
72
+ data = response.text
73
+
74
+ if response.status_code in {401, 403}:
75
+ raise APIError(f"API authentication failed: HTTP {response.status_code}. Check your API key.")
76
+ if response.status_code == 429:
77
+ raise APIError("API rate limit exceeded. Retry later or increase --poll-interval for async tasks.")
78
+ if response.status_code >= 400:
79
+ message = data if isinstance(data, str) else json.dumps(mask_mapping(data), ensure_ascii=False)
80
+ raise APIError(f"API request failed: HTTP {response.status_code}. {message}")
81
+
82
+ return APIResponse(data=data, status_code=response.status_code, elapsed_seconds=elapsed, url=endpoint)
File without changes
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,5 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The CLI currently registers this tool through the generic, definition-driven
4
+ adapter layer in sv_cli.main.
5
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,5 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The CLI currently registers this tool through the generic, definition-driven
4
+ adapter layer in sv_cli.main.
5
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """
@@ -0,0 +1,5 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The CLI currently registers this tool through the generic, definition-driven
4
+ adapter layer in sv_cli.main.
5
+ """
@@ -0,0 +1,4 @@
1
+ """Command module reserved for future adapter-specific customizations.
2
+
3
+ The initial CLI registers generic, definition-driven commands in sv_cli.main.
4
+ """