wildberries-cli 0.1.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.
- wildberries_cli/__init__.py +1 -0
- wildberries_cli/args.py +65 -0
- wildberries_cli/client.py +159 -0
- wildberries_cli/commands/__init__.py +1 -0
- wildberries_cli/commands/communications.py +128 -0
- wildberries_cli/commands/config_cmd.py +91 -0
- wildberries_cli/commands/general.py +45 -0
- wildberries_cli/commands/orders_fbs.py +117 -0
- wildberries_cli/commands/products.py +92 -0
- wildberries_cli/commands/raw.py +71 -0
- wildberries_cli/commands/reports.py +61 -0
- wildberries_cli/commands/tariffs.py +60 -0
- wildberries_cli/config.py +169 -0
- wildberries_cli/main.py +97 -0
- wildberries_cli/output.py +192 -0
- wildberries_cli/serialize.py +63 -0
- wildberries_cli-0.1.0.dist-info/METADATA +224 -0
- wildberries_cli-0.1.0.dist-info/RECORD +20 -0
- wildberries_cli-0.1.0.dist-info/WHEEL +4 -0
- wildberries_cli-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""wildberries-cli package."""
|
wildberries_cli/args.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""CLI argument parsing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Iterable
|
|
9
|
+
|
|
10
|
+
from wildberries_cli.output import read_text_arg
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_json_input(body_json: str | None = None, body_file: str | None = None) -> Any:
|
|
14
|
+
"""Load JSON from one source (`--body-json` or `--body-file`)."""
|
|
15
|
+
if body_json and body_file:
|
|
16
|
+
raise ValueError("Use only one of --body-json or --body-file")
|
|
17
|
+
if body_json is None and body_file is None:
|
|
18
|
+
raise ValueError("Body JSON is required")
|
|
19
|
+
|
|
20
|
+
if body_json is not None:
|
|
21
|
+
text = read_text_arg(body_json)
|
|
22
|
+
else:
|
|
23
|
+
if body_file == "-":
|
|
24
|
+
text = read_text_arg("-")
|
|
25
|
+
else:
|
|
26
|
+
text = Path(str(body_file)).read_text()
|
|
27
|
+
|
|
28
|
+
return json.loads(text)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_rfc3339ish(value: str) -> datetime:
|
|
32
|
+
"""Parse an RFC3339-ish datetime/date string accepted by the SDK."""
|
|
33
|
+
text = value.strip()
|
|
34
|
+
if text.endswith("Z"):
|
|
35
|
+
text = text[:-1] + "+00:00"
|
|
36
|
+
try:
|
|
37
|
+
return datetime.fromisoformat(text)
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
raise ValueError(f"Invalid datetime '{value}'. Use ISO/RFC3339 (e.g. 2024-01-01 or 2024-01-01T12:00:00+03:00).") from exc
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_kv_pairs(items: Iterable[str]) -> dict[str, str]:
|
|
43
|
+
out: dict[str, str] = {}
|
|
44
|
+
for item in items:
|
|
45
|
+
key, value = _split_kv(item)
|
|
46
|
+
out[key] = value
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_json_kv_pairs(items: Iterable[str]) -> dict[str, Any]:
|
|
51
|
+
out: dict[str, Any] = {}
|
|
52
|
+
for item in items:
|
|
53
|
+
key, value = _split_kv(item)
|
|
54
|
+
out[key] = json.loads(value)
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _split_kv(item: str) -> tuple[str, str]:
|
|
59
|
+
if "=" not in item:
|
|
60
|
+
raise ValueError(f"Expected KEY=VALUE, got '{item}'")
|
|
61
|
+
key, value = item.split("=", 1)
|
|
62
|
+
key = key.strip()
|
|
63
|
+
if not key:
|
|
64
|
+
raise ValueError(f"Expected KEY=VALUE, got '{item}'")
|
|
65
|
+
return key, value
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Dynamic Wildberries SDK client factory and API invocation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import inspect
|
|
7
|
+
import pkgutil
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
import wildberries_sdk
|
|
14
|
+
|
|
15
|
+
from wildberries_cli.output import print_error
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from wildberries_cli.config import Config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ModuleClient:
|
|
23
|
+
module_name: str
|
|
24
|
+
sdk_module: Any
|
|
25
|
+
api: Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def list_sdk_modules() -> list[str]:
|
|
29
|
+
return sorted(m.name for m in pkgutil.iter_modules(wildberries_sdk.__path__))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def list_methods(module_name: str) -> list[str]:
|
|
33
|
+
cls = _get_default_api_class(normalize_module_name(module_name))
|
|
34
|
+
methods = []
|
|
35
|
+
for name, fn in inspect.getmembers(cls, inspect.isfunction):
|
|
36
|
+
if name.startswith("_"):
|
|
37
|
+
continue
|
|
38
|
+
methods.append(name)
|
|
39
|
+
return sorted(methods)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def method_signature(module_name: str, method_name: str) -> str:
|
|
43
|
+
cls = _get_default_api_class(normalize_module_name(module_name))
|
|
44
|
+
fn = getattr(cls, method_name, None)
|
|
45
|
+
if fn is None or not callable(fn):
|
|
46
|
+
raise AttributeError(f"Method '{method_name}' not found in module '{module_name}'")
|
|
47
|
+
return f"{method_name}{inspect.signature(fn)}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_module_client(module_name: str, cfg: "Config", *, require_token: bool = True) -> ModuleClient:
|
|
51
|
+
module_name = normalize_module_name(module_name)
|
|
52
|
+
if require_token:
|
|
53
|
+
_require_token(cfg)
|
|
54
|
+
|
|
55
|
+
sdk_module = importlib.import_module(f"wildberries_sdk.{module_name}")
|
|
56
|
+
Configuration = getattr(sdk_module, "Configuration")
|
|
57
|
+
ApiClient = getattr(sdk_module, "ApiClient")
|
|
58
|
+
DefaultApi = getattr(sdk_module, "DefaultApi")
|
|
59
|
+
|
|
60
|
+
conf_kwargs: dict[str, Any] = {}
|
|
61
|
+
if cfg.api_token:
|
|
62
|
+
conf_kwargs["api_key"] = {"HeaderApiKey": cfg.api_token}
|
|
63
|
+
if cfg.retries:
|
|
64
|
+
conf_kwargs["retries"] = cfg.retries
|
|
65
|
+
conf = Configuration(**conf_kwargs)
|
|
66
|
+
api_client = ApiClient(conf)
|
|
67
|
+
api = DefaultApi(api_client)
|
|
68
|
+
return ModuleClient(module_name=module_name, sdk_module=sdk_module, api=api)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def call_api(
|
|
72
|
+
module_name: str,
|
|
73
|
+
method_name: str,
|
|
74
|
+
cfg: "Config",
|
|
75
|
+
*,
|
|
76
|
+
require_token: bool = True,
|
|
77
|
+
**kwargs: Any,
|
|
78
|
+
) -> Any:
|
|
79
|
+
client = get_module_client(module_name, cfg, require_token=require_token)
|
|
80
|
+
fn = getattr(client.api, method_name, None)
|
|
81
|
+
if fn is None or not callable(fn):
|
|
82
|
+
print_error("validation_error", f"Unknown method '{method_name}' for module '{client.module_name}'")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
return call_with_retry(fn, cfg, **kwargs)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def call_with_retry(fn: Any, cfg: "Config", **kwargs: Any) -> Any:
|
|
88
|
+
sig = inspect.signature(fn)
|
|
89
|
+
if "_request_timeout" in sig.parameters and "_request_timeout" not in kwargs:
|
|
90
|
+
kwargs["_request_timeout"] = cfg.timeout_seconds
|
|
91
|
+
|
|
92
|
+
max_attempts = max(1, int(cfg.retries))
|
|
93
|
+
for attempt in range(max_attempts):
|
|
94
|
+
try:
|
|
95
|
+
return fn(**kwargs)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
status = getattr(exc, "status", None)
|
|
98
|
+
if status in {429, 500, 502, 503, 504} and attempt < max_attempts - 1:
|
|
99
|
+
delay = _retry_after_seconds(exc) or min(2 ** attempt, 10)
|
|
100
|
+
time.sleep(delay)
|
|
101
|
+
continue
|
|
102
|
+
_handle_exception(exc)
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
raise typer.Exit(1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def normalize_module_name(value: str) -> str:
|
|
109
|
+
return value.replace("-", "_").strip()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _require_token(cfg: "Config") -> None:
|
|
113
|
+
if not cfg.api_token:
|
|
114
|
+
print_error(
|
|
115
|
+
"auth_error",
|
|
116
|
+
"No WB API token configured. Set WB_API_TOKEN, use --api-token, or run `wb config init`.",
|
|
117
|
+
)
|
|
118
|
+
raise typer.Exit(1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _get_default_api_class(module_name: str) -> Any:
|
|
122
|
+
sdk_module = importlib.import_module(f"wildberries_sdk.{module_name}")
|
|
123
|
+
return getattr(sdk_module, "DefaultApi")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _retry_after_seconds(exc: Exception) -> int | None:
|
|
127
|
+
headers = getattr(exc, "headers", None)
|
|
128
|
+
if not headers:
|
|
129
|
+
return None
|
|
130
|
+
try:
|
|
131
|
+
value = headers.get("Retry-After") # type: ignore[union-attr]
|
|
132
|
+
if value is None:
|
|
133
|
+
return None
|
|
134
|
+
return max(1, int(value))
|
|
135
|
+
except Exception:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _handle_exception(exc: Exception) -> None:
|
|
140
|
+
status = getattr(exc, "status", None)
|
|
141
|
+
message = str(exc)
|
|
142
|
+
detail = getattr(exc, "body", None)
|
|
143
|
+
|
|
144
|
+
if status == 401:
|
|
145
|
+
print_error("auth_error", "Authentication failed. Check WB_API_TOKEN.", status_code=status, detail=detail)
|
|
146
|
+
return
|
|
147
|
+
if status == 403:
|
|
148
|
+
print_error("forbidden", "Permission denied or token lacks required scope.", status_code=status, detail=detail)
|
|
149
|
+
return
|
|
150
|
+
if status == 404:
|
|
151
|
+
print_error("not_found", "Resource not found.", status_code=status, detail=detail)
|
|
152
|
+
return
|
|
153
|
+
if status == 429:
|
|
154
|
+
print_error("rate_limit", "Rate limit exceeded.", status_code=status, detail=detail)
|
|
155
|
+
return
|
|
156
|
+
if status is not None:
|
|
157
|
+
print_error("api_error", message, status_code=status, detail=detail)
|
|
158
|
+
return
|
|
159
|
+
print_error("cli_error", message)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""wb command modules."""
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""communications subcommand (feedbacks/questions)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from wildberries_cli.client import call_api
|
|
8
|
+
from wildberries_cli.config import Config
|
|
9
|
+
from wildberries_cli.output import emit, feedbacks_table, print_error, questions_table, read_text_arg
|
|
10
|
+
from wildberries_cli.serialize import to_data
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(name="communications", help="Buyer communications APIs.", no_args_is_help=True)
|
|
13
|
+
feedbacks_app = typer.Typer(name="feedbacks", help="Feedbacks API.", no_args_is_help=True)
|
|
14
|
+
questions_app = typer.Typer(name="questions", help="Questions API.", no_args_is_help=True)
|
|
15
|
+
app.add_typer(feedbacks_app, name="feedbacks")
|
|
16
|
+
app.add_typer(questions_app, name="questions")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@feedbacks_app.command("list")
|
|
20
|
+
def feedbacks_list(
|
|
21
|
+
ctx: typer.Context,
|
|
22
|
+
answered: bool = typer.Option(..., "--answered/--unanswered", help="Filter by answered state"),
|
|
23
|
+
take: int = typer.Option(100, "--take", min=1, max=5000, help="Number of feedbacks to return"),
|
|
24
|
+
skip: int = typer.Option(0, "--skip", min=0, help="Offset"),
|
|
25
|
+
nm_id: int | None = typer.Option(None, "--nm-id", help="WB nmID"),
|
|
26
|
+
order: str | None = typer.Option(None, "--order", help="dateAsc or dateDesc"),
|
|
27
|
+
date_from: int | None = typer.Option(None, "--date-from", help="Unix timestamp start"),
|
|
28
|
+
date_to: int | None = typer.Option(None, "--date-to", help="Unix timestamp end"),
|
|
29
|
+
) -> None:
|
|
30
|
+
cfg: Config = ctx.obj
|
|
31
|
+
kwargs = {
|
|
32
|
+
"is_answered": answered,
|
|
33
|
+
"take": take,
|
|
34
|
+
"skip": skip,
|
|
35
|
+
"nm_id": nm_id,
|
|
36
|
+
"order": order,
|
|
37
|
+
"date_from": date_from,
|
|
38
|
+
"date_to": date_to,
|
|
39
|
+
}
|
|
40
|
+
data = to_data(call_api("communications", "api_v1_feedbacks_get", cfg, **{k: v for k, v in kwargs.items() if v is not None}))
|
|
41
|
+
emit(data, pretty=cfg.pretty, table_builder=feedbacks_table)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@feedbacks_app.command("get")
|
|
45
|
+
def feedbacks_get(ctx: typer.Context, feedback_id: str = typer.Argument(..., help="Feedback ID")) -> None:
|
|
46
|
+
cfg: Config = ctx.obj
|
|
47
|
+
data = to_data(call_api("communications", "api_v1_feedback_get", cfg, id=feedback_id))
|
|
48
|
+
emit(data, pretty=cfg.pretty)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@feedbacks_app.command("answer")
|
|
52
|
+
def feedbacks_answer(
|
|
53
|
+
ctx: typer.Context,
|
|
54
|
+
feedback_id: str = typer.Argument(..., help="Feedback ID"),
|
|
55
|
+
text: str = typer.Option(..., "--text", help="Answer text or '-' for stdin"),
|
|
56
|
+
) -> None:
|
|
57
|
+
cfg: Config = ctx.obj
|
|
58
|
+
try:
|
|
59
|
+
from wildberries_sdk.communications import ApiV1FeedbacksAnswerPostRequest
|
|
60
|
+
|
|
61
|
+
req = ApiV1FeedbacksAnswerPostRequest(id=feedback_id, text=read_text_arg(text))
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
print_error("validation_error", f"Invalid feedback answer payload: {exc}")
|
|
64
|
+
raise typer.Exit(1)
|
|
65
|
+
|
|
66
|
+
data = to_data(call_api("communications", "api_v1_feedbacks_answer_post", cfg, api_v1_feedbacks_answer_post_request=req))
|
|
67
|
+
emit(data if data is not None else {"ok": True, "id": feedback_id}, pretty=cfg.pretty)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@questions_app.command("list")
|
|
71
|
+
def questions_list(
|
|
72
|
+
ctx: typer.Context,
|
|
73
|
+
answered: bool = typer.Option(..., "--answered/--unanswered", help="Filter by answered state"),
|
|
74
|
+
take: int = typer.Option(100, "--take", min=1, max=10000, help="Number of questions to return"),
|
|
75
|
+
skip: int = typer.Option(0, "--skip", min=0, help="Offset"),
|
|
76
|
+
nm_id: int | None = typer.Option(None, "--nm-id", help="WB nmID"),
|
|
77
|
+
order: str | None = typer.Option(None, "--order", help="dateAsc or dateDesc"),
|
|
78
|
+
date_from: int | None = typer.Option(None, "--date-from", help="Unix timestamp start"),
|
|
79
|
+
date_to: int | None = typer.Option(None, "--date-to", help="Unix timestamp end"),
|
|
80
|
+
) -> None:
|
|
81
|
+
cfg: Config = ctx.obj
|
|
82
|
+
kwargs = {
|
|
83
|
+
"is_answered": answered,
|
|
84
|
+
"take": take,
|
|
85
|
+
"skip": skip,
|
|
86
|
+
"nm_id": nm_id,
|
|
87
|
+
"order": order,
|
|
88
|
+
"date_from": date_from,
|
|
89
|
+
"date_to": date_to,
|
|
90
|
+
}
|
|
91
|
+
data = to_data(call_api("communications", "api_v1_questions_get", cfg, **{k: v for k, v in kwargs.items() if v is not None}))
|
|
92
|
+
emit(data, pretty=cfg.pretty, table_builder=questions_table)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@questions_app.command("get")
|
|
96
|
+
def questions_get(ctx: typer.Context, question_id: str = typer.Argument(..., help="Question ID")) -> None:
|
|
97
|
+
cfg: Config = ctx.obj
|
|
98
|
+
data = to_data(call_api("communications", "api_v1_question_get", cfg, id=question_id))
|
|
99
|
+
emit(data, pretty=cfg.pretty)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@questions_app.command("answer")
|
|
103
|
+
def questions_answer(
|
|
104
|
+
ctx: typer.Context,
|
|
105
|
+
question_id: str = typer.Argument(..., help="Question ID"),
|
|
106
|
+
text: str = typer.Option(..., "--text", help="Answer text or '-' for stdin"),
|
|
107
|
+
state: str = typer.Option("wbRu", "--state", help="Question state (`wbRu` to publish answer, `none` to reject)")
|
|
108
|
+
) -> None:
|
|
109
|
+
cfg: Config = ctx.obj
|
|
110
|
+
try:
|
|
111
|
+
from wildberries_sdk.communications import (
|
|
112
|
+
ApiV1QuestionsPatchRequest,
|
|
113
|
+
ApiV1QuestionsPatchRequestOneOf1,
|
|
114
|
+
ApiV1QuestionsPatchRequestOneOf1Answer,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
payload = ApiV1QuestionsPatchRequestOneOf1(
|
|
118
|
+
id=question_id,
|
|
119
|
+
answer=ApiV1QuestionsPatchRequestOneOf1Answer(text=read_text_arg(text)),
|
|
120
|
+
state=state,
|
|
121
|
+
)
|
|
122
|
+
req = ApiV1QuestionsPatchRequest(actual_instance=payload)
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
print_error("validation_error", f"Invalid question answer payload: {exc}")
|
|
125
|
+
raise typer.Exit(1)
|
|
126
|
+
|
|
127
|
+
data = to_data(call_api("communications", "api_v1_questions_patch", cfg, api_v1_questions_patch_request=req))
|
|
128
|
+
emit(data, pretty=cfg.pretty)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""config subcommand: show / set / init."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.prompt import Prompt
|
|
10
|
+
|
|
11
|
+
from wildberries_cli.client import call_api
|
|
12
|
+
from wildberries_cli.config import Config, CONFIG_PATH, config_as_dict, load_config, save_config, save_config_key
|
|
13
|
+
from wildberries_cli.output import print_error, print_json
|
|
14
|
+
from wildberries_cli.serialize import to_data
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(name="config", help="Manage wb CLI configuration.", no_args_is_help=True)
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("show")
|
|
21
|
+
def config_show(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
reveal: bool = typer.Option(False, "--reveal", help="Show full API token."),
|
|
24
|
+
) -> None:
|
|
25
|
+
cfg: Config = ctx.obj
|
|
26
|
+
print_json(config_as_dict(cfg, reveal=reveal))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("set")
|
|
30
|
+
def config_set(
|
|
31
|
+
key: str = typer.Argument(..., help="Dotted config key, e.g. core.retries"),
|
|
32
|
+
value: str = typer.Argument(..., help="Value to set"),
|
|
33
|
+
) -> None:
|
|
34
|
+
try:
|
|
35
|
+
save_config_key(key, value)
|
|
36
|
+
print_json({"ok": True, "key": key, "value": value})
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
print_error("config_error", f"Failed to write config: {exc}")
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("init")
|
|
43
|
+
def config_init(ctx: typer.Context) -> None:
|
|
44
|
+
if not sys.stdin.isatty():
|
|
45
|
+
print_error("validation_error", "`wb config init` requires an interactive terminal.")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
|
|
48
|
+
cfg: Config = ctx.obj
|
|
49
|
+
console.print("[bold]wb setup wizard[/bold]")
|
|
50
|
+
console.print(f"Config will be saved to: [dim]{CONFIG_PATH}[/dim]\n")
|
|
51
|
+
|
|
52
|
+
api_token = Prompt.ask("WB API token", password=True, default=cfg.api_token or "")
|
|
53
|
+
timeout_text = Prompt.ask("Request timeout (seconds)", default=str(cfg.timeout_seconds))
|
|
54
|
+
retries_text = Prompt.ask("Retry attempts", default=str(cfg.retries))
|
|
55
|
+
locale_text = Prompt.ask("Default locale (ru/en/zh, optional)", default=cfg.locale or "")
|
|
56
|
+
|
|
57
|
+
if not api_token:
|
|
58
|
+
print_error("validation_error", "WB API token is required.")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
timeout_seconds = float(timeout_text)
|
|
63
|
+
retries = int(retries_text)
|
|
64
|
+
except ValueError:
|
|
65
|
+
print_error("validation_error", "Timeout must be a number and retries must be an integer.")
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
|
|
68
|
+
new_cfg = load_config()
|
|
69
|
+
new_cfg.api_token = api_token
|
|
70
|
+
new_cfg.timeout_seconds = timeout_seconds
|
|
71
|
+
new_cfg.retries = retries
|
|
72
|
+
new_cfg.locale = locale_text or None
|
|
73
|
+
|
|
74
|
+
console.print("\nValidating token with `general.api_v1_seller_info_get`…")
|
|
75
|
+
try:
|
|
76
|
+
result = call_api("general", "api_v1_seller_info_get", new_cfg)
|
|
77
|
+
seller = to_data(result)
|
|
78
|
+
seller_name = seller.get("name") if isinstance(seller, dict) else None
|
|
79
|
+
if seller_name:
|
|
80
|
+
console.print(f"[green]✓[/green] Connected as: {seller_name}")
|
|
81
|
+
else:
|
|
82
|
+
console.print("[green]✓[/green] Token is valid")
|
|
83
|
+
except typer.Exit:
|
|
84
|
+
raise
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
print_error("auth_error", f"Validation failed: {exc}")
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
|
|
89
|
+
save_config(new_cfg)
|
|
90
|
+
console.print(f"\n[green]✓[/green] Config saved to {CONFIG_PATH}")
|
|
91
|
+
print_json(config_as_dict(new_cfg))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""general subcommand: ping / seller-info / users."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from wildberries_cli.client import call_api
|
|
8
|
+
from wildberries_cli.config import Config
|
|
9
|
+
from wildberries_cli.output import emit
|
|
10
|
+
from wildberries_cli.serialize import to_data
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(name="general", help="General Wildberries seller APIs.", no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("ping")
|
|
16
|
+
def ping(ctx: typer.Context) -> None:
|
|
17
|
+
cfg: Config = ctx.obj
|
|
18
|
+
data = to_data(call_api("general", "ping_get", cfg, require_token=False))
|
|
19
|
+
emit(data, pretty=cfg.pretty)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command("seller-info")
|
|
23
|
+
def seller_info(ctx: typer.Context) -> None:
|
|
24
|
+
cfg: Config = ctx.obj
|
|
25
|
+
data = to_data(call_api("general", "api_v1_seller_info_get", cfg))
|
|
26
|
+
emit(data, pretty=cfg.pretty)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("users")
|
|
30
|
+
def users_list(
|
|
31
|
+
ctx: typer.Context,
|
|
32
|
+
limit: int | None = typer.Option(None, "--limit", help="Max users to return (<=100)"),
|
|
33
|
+
offset: int | None = typer.Option(None, "--offset", help="Pagination offset"),
|
|
34
|
+
invited_only: bool | None = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--invited-only/--active",
|
|
37
|
+
help="List invited (not activated) users or active users",
|
|
38
|
+
),
|
|
39
|
+
) -> None:
|
|
40
|
+
cfg: Config = ctx.obj
|
|
41
|
+
kwargs = {"limit": limit, "offset": offset}
|
|
42
|
+
if invited_only is not None:
|
|
43
|
+
kwargs["is_invite_only"] = invited_only
|
|
44
|
+
data = to_data(call_api("general", "api_v1_users_get", cfg, **{k: v for k, v in kwargs.items() if v is not None}))
|
|
45
|
+
emit(data, pretty=cfg.pretty)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""orders-fbs subcommand (selected operational endpoints)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from wildberries_cli.client import call_api
|
|
8
|
+
from wildberries_cli.config import Config
|
|
9
|
+
from wildberries_cli.output import emit, fbs_orders_table, print_error
|
|
10
|
+
from wildberries_cli.serialize import to_data
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(name="orders-fbs", help="Orders FBS APIs (selected endpoints).", no_args_is_help=True)
|
|
13
|
+
orders_app = typer.Typer(name="orders", help="FBS orders.", no_args_is_help=True)
|
|
14
|
+
supplies_app = typer.Typer(name="supplies", help="FBS supplies.", no_args_is_help=True)
|
|
15
|
+
app.add_typer(orders_app, name="orders")
|
|
16
|
+
app.add_typer(supplies_app, name="supplies")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@orders_app.command("new")
|
|
20
|
+
def orders_new(ctx: typer.Context) -> None:
|
|
21
|
+
cfg: Config = ctx.obj
|
|
22
|
+
data = to_data(call_api("orders_fbs", "api_v3_orders_new_get", cfg))
|
|
23
|
+
emit(data, pretty=cfg.pretty, table_builder=fbs_orders_table)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@orders_app.command("list")
|
|
27
|
+
def orders_list(
|
|
28
|
+
ctx: typer.Context,
|
|
29
|
+
limit: int = typer.Option(100, "--limit", min=1, max=1000),
|
|
30
|
+
next: int = typer.Option(0, "--next", help="Pagination cursor"),
|
|
31
|
+
date_from: int | None = typer.Option(None, "--date-from", help="Unix timestamp start"),
|
|
32
|
+
date_to: int | None = typer.Option(None, "--date-to", help="Unix timestamp end"),
|
|
33
|
+
) -> None:
|
|
34
|
+
cfg: Config = ctx.obj
|
|
35
|
+
kwargs = {"limit": limit, "next": next, "date_from": date_from, "date_to": date_to}
|
|
36
|
+
data = to_data(call_api("orders_fbs", "api_v3_orders_get", cfg, **{k: v for k, v in kwargs.items() if v is not None}))
|
|
37
|
+
emit(data, pretty=cfg.pretty, table_builder=fbs_orders_table)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@orders_app.command("status")
|
|
41
|
+
def orders_status(
|
|
42
|
+
ctx: typer.Context,
|
|
43
|
+
order_ids: list[int] = typer.Option(..., "--order", help="Order ID(s). Repeat option up to API limit."),
|
|
44
|
+
) -> None:
|
|
45
|
+
cfg: Config = ctx.obj
|
|
46
|
+
try:
|
|
47
|
+
from wildberries_sdk.orders_fbs.models.api_v3_orders_status_post_request import ApiV3OrdersStatusPostRequest
|
|
48
|
+
|
|
49
|
+
req = ApiV3OrdersStatusPostRequest(orders=order_ids)
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
print_error("validation_error", f"Invalid status request: {exc}")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
data = to_data(call_api("orders_fbs", "api_v3_orders_status_post", cfg, api_v3_orders_status_post_request=req))
|
|
55
|
+
emit(data, pretty=cfg.pretty)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@orders_app.command("stickers")
|
|
59
|
+
def orders_stickers(
|
|
60
|
+
ctx: typer.Context,
|
|
61
|
+
order_ids: list[int] = typer.Option(..., "--order", help="Order ID(s). Repeat option."),
|
|
62
|
+
type: str = typer.Option(..., "--type", help="Sticker type"),
|
|
63
|
+
width: int = typer.Option(..., "--width", help="Sticker width"),
|
|
64
|
+
height: int = typer.Option(..., "--height", help="Sticker height"),
|
|
65
|
+
) -> None:
|
|
66
|
+
cfg: Config = ctx.obj
|
|
67
|
+
try:
|
|
68
|
+
from wildberries_sdk.orders_fbs.models.api_v3_orders_stickers_post_request import (
|
|
69
|
+
ApiV3OrdersStickersPostRequest,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
req = ApiV3OrdersStickersPostRequest(orders=order_ids)
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
print_error("validation_error", f"Invalid stickers request: {exc}")
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
|
|
77
|
+
data = to_data(
|
|
78
|
+
call_api(
|
|
79
|
+
"orders_fbs",
|
|
80
|
+
"api_v3_orders_stickers_post",
|
|
81
|
+
cfg,
|
|
82
|
+
type=type,
|
|
83
|
+
width=width,
|
|
84
|
+
height=height,
|
|
85
|
+
api_v3_orders_stickers_post_request=req,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
emit(data, pretty=cfg.pretty)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@supplies_app.command("list")
|
|
92
|
+
def supplies_list(
|
|
93
|
+
ctx: typer.Context,
|
|
94
|
+
limit: int = typer.Option(100, "--limit", min=1, max=1000),
|
|
95
|
+
next: int = typer.Option(0, "--next", help="Pagination cursor"),
|
|
96
|
+
) -> None:
|
|
97
|
+
cfg: Config = ctx.obj
|
|
98
|
+
data = to_data(call_api("orders_fbs", "api_v3_supplies_get", cfg, limit=limit, next=next))
|
|
99
|
+
emit(data, pretty=cfg.pretty)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@supplies_app.command("create")
|
|
103
|
+
def supplies_create(
|
|
104
|
+
ctx: typer.Context,
|
|
105
|
+
name: str = typer.Option(..., "--name", help="Supply name"),
|
|
106
|
+
) -> None:
|
|
107
|
+
cfg: Config = ctx.obj
|
|
108
|
+
try:
|
|
109
|
+
from wildberries_sdk.orders_fbs.models.api_v3_supplies_post_request import ApiV3SuppliesPostRequest
|
|
110
|
+
|
|
111
|
+
req = ApiV3SuppliesPostRequest(name=name)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
print_error("validation_error", f"Invalid supply request: {exc}")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
|
|
116
|
+
data = to_data(call_api("orders_fbs", "api_v3_supplies_post", cfg, api_v3_supplies_post_request=req))
|
|
117
|
+
emit(data, pretty=cfg.pretty)
|