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.
- sv_cli/__init__.py +3 -0
- sv_cli/adapters.py +323 -0
- sv_cli/api_client.py +82 -0
- sv_cli/commands/__init__.py +0 -0
- sv_cli/commands/auth.py +4 -0
- sv_cli/commands/better_keywords.py +4 -0
- sv_cli/commands/call.py +4 -0
- sv_cli/commands/config.py +4 -0
- sv_cli/commands/content_quality.py +5 -0
- sv_cli/commands/content_transformer.py +4 -0
- sv_cli/commands/core_analysis.py +4 -0
- sv_cli/commands/definitions.py +4 -0
- sv_cli/commands/geo_audit.py +4 -0
- sv_cli/commands/insight_igniter.py +4 -0
- sv_cli/commands/marketplace_services.py +5 -0
- sv_cli/commands/options.py +4 -0
- sv_cli/commands/preliminary_audit.py +4 -0
- sv_cli/commands/ranklens.py +4 -0
- sv_cli/commands/seo_image.py +4 -0
- sv_cli/commands/seo_mapping.py +4 -0
- sv_cli/commands/seogpt.py +4 -0
- sv_cli/commands/seogpt2.py +4 -0
- sv_cli/commands/seogpt_compare.py +4 -0
- sv_cli/commands/top_competitors.py +5 -0
- sv_cli/commands/topical_authority.py +4 -0
- sv_cli/config.py +216 -0
- sv_cli/definitions.py +283 -0
- sv_cli/errors.py +55 -0
- sv_cli/executor.py +188 -0
- sv_cli/formatter.py +227 -0
- sv_cli/main.py +905 -0
- sv_cli/renderers/__init__.py +0 -0
- sv_cli/renderers/csv_renderer.py +9 -0
- sv_cli/renderers/json_renderer.py +10 -0
- sv_cli/renderers/markdown_renderer.py +9 -0
- sv_cli/renderers/table_renderer.py +9 -0
- sv_cli/renderers/text_renderer.py +9 -0
- sv_cli/resolver.py +523 -0
- sv_cli/schemas/__init__.py +0 -0
- sv_cli/schemas/api_response.py +12 -0
- sv_cli/schemas/config.py +13 -0
- sv_cli/schemas/tool_definition.py +17 -0
- sv_cli/tasks.py +168 -0
- sv_cli/utils.py +204 -0
- sv_cli-0.3.0.dist-info/METADATA +338 -0
- sv_cli-0.3.0.dist-info/RECORD +49 -0
- sv_cli-0.3.0.dist-info/WHEEL +4 -0
- sv_cli-0.3.0.dist-info/entry_points.txt +2 -0
- sv_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
sv_cli/__init__.py
ADDED
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
|
sv_cli/commands/auth.py
ADDED
sv_cli/commands/call.py
ADDED