venice-cli 0.14.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.
@@ -0,0 +1,132 @@
1
+ """Shared UX/budget rails for spend-incurring commands.
2
+
3
+ Extracted from `image` so it, `upscale`, and `bg-remove` share one copy of the
4
+ estimate/confirm/status/write plumbing rather than each carrying its own. These
5
+ helpers take primitive args (cost, max_spend, err, byte blobs) so they stay
6
+ independent of any one command's argument shape.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import List, Optional
13
+
14
+ from .. import billing
15
+ from ..client import VeniceAPIError
16
+
17
+
18
+ def resolve_output(arg_output: Optional[Path], default_name: str) -> Path:
19
+ """Pick an output path: an explicit file, a file inside an explicit dir, or
20
+ `default_name` in the cwd."""
21
+ if arg_output is None:
22
+ return Path.cwd() / default_name
23
+ if arg_output.is_dir():
24
+ return arg_output / default_name
25
+ return arg_output
26
+
27
+
28
+ def print_estimate(cost: Optional[float], label: str) -> None:
29
+ if cost is None:
30
+ print(f"Estimated cost: (unknown — {label})", file=sys.stderr)
31
+ else:
32
+ print(
33
+ f"Estimated cost: {billing.format_usd(cost)} ({label})",
34
+ file=sys.stderr,
35
+ )
36
+
37
+
38
+ def print_balance_and_remaining(client, cost: Optional[float], *, show: bool) -> None:
39
+ if not show:
40
+ return
41
+ try:
42
+ info = billing.fetch_balance(client)
43
+ except VeniceAPIError:
44
+ info = None
45
+ if not info or info.get("total") is None:
46
+ return
47
+ print(f"Balance: {billing.format_balance_breakdown(info)}", file=sys.stderr)
48
+ if cost is not None:
49
+ try:
50
+ remaining = float(info["total"]) - float(cost)
51
+ print(f"After charge: {billing.format_usd(remaining)}", file=sys.stderr)
52
+ except (TypeError, ValueError):
53
+ pass
54
+
55
+
56
+ def over_budget(cost: Optional[float], max_spend: Optional[float]) -> bool:
57
+ if max_spend is None or cost is None:
58
+ return False
59
+ try:
60
+ return float(cost) > float(max_spend)
61
+ except (TypeError, ValueError):
62
+ return False
63
+
64
+
65
+ def confirm_or_exit(yes: bool) -> Optional[int]:
66
+ if yes:
67
+ return None
68
+ if not sys.stdin.isatty():
69
+ print("non-interactive; pass --yes to confirm the charge.", file=sys.stderr)
70
+ return 1
71
+ try:
72
+ ans = input("Proceed? [y/N] ").strip().lower()
73
+ except EOFError:
74
+ ans = ""
75
+ if ans not in ("y", "yes"):
76
+ print("aborted by user", file=sys.stderr)
77
+ return 1
78
+ return None
79
+
80
+
81
+ def status_to_exit(err: VeniceAPIError) -> int:
82
+ s = err.status
83
+ if s == 400:
84
+ return 2
85
+ if s == 402:
86
+ return 1 # insufficient balance ~ declined
87
+ if s == 422:
88
+ return 3
89
+ if s == 429:
90
+ return 4
91
+ if s == 503:
92
+ return 5
93
+ if 500 <= s < 600:
94
+ return 5
95
+ if s == 404:
96
+ return 6
97
+ if s == 0:
98
+ return 8
99
+ return 2
100
+
101
+
102
+ def write_bytes_outputs(blobs: List[bytes], paths: List[Path]) -> Optional[int]:
103
+ """Write byte blobs to paths. Returns an exit code on failure, else None."""
104
+ for data, path in zip(blobs, paths):
105
+ try:
106
+ path.write_bytes(data)
107
+ except OSError as e:
108
+ print(f"could not write {path}: {e}", file=sys.stderr)
109
+ return 9
110
+ abs_path = path.resolve()
111
+ print(str(abs_path))
112
+ print(f"wrote {len(data)} bytes to {abs_path}", file=sys.stderr)
113
+ return None
114
+
115
+
116
+ def post_binary_op(client, endpoint: str, body: dict, out_path: Path, label: str) -> int:
117
+ """POST a JSON body to an endpoint that returns raw image bytes and write
118
+ them to `out_path`.
119
+
120
+ Returns an exit code (0 on success). API errors map through
121
+ `status_to_exit`; a JSON (non-image) 200 is treated as an unexpected
122
+ server response. `label` prefixes error messages (e.g. "upscale").
123
+ """
124
+ try:
125
+ _ctype, payload = client.post_for_bytes_or_json(endpoint, body)
126
+ except VeniceAPIError as e:
127
+ print(f"{label} failed: {e}", file=sys.stderr)
128
+ return status_to_exit(e)
129
+ if isinstance(payload, (bytes, bytearray)):
130
+ return write_bytes_outputs([bytes(payload)], [out_path]) or 0
131
+ print(f"{label}: unexpected non-image response: {payload!r}", file=sys.stderr)
132
+ return 5
@@ -0,0 +1,124 @@
1
+ """`venice balance` -- show current account balance and tier."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sys
6
+
7
+ from .. import auth
8
+ from ..billing import SPEND_ORDER, fetch_balance, format_balance_breakdown, format_usd
9
+ from ..client import VeniceAPIError
10
+
11
+
12
+ def register(subparsers) -> None:
13
+ p = subparsers.add_parser(
14
+ "balance",
15
+ help="Show current spendable balance (USD + DIEM credit).",
16
+ description=(
17
+ "Queries /api_keys/rate_limits. Spendable balance = USD + DIEM "
18
+ "(1 DIEM = $1 of purchasing power). Default: prints combined "
19
+ "total '$X.XX USD' to stdout. --json for raw, --verbose for "
20
+ "tier + breakdown + epoch. --min sets a floor: exits 1 if total "
21
+ "is below it (script-friendly)."
22
+ ),
23
+ )
24
+ p.add_argument(
25
+ "--json",
26
+ action="store_true",
27
+ help="Dump raw {USD, DIEM, tier, next_epoch, key_expires} to stdout.",
28
+ )
29
+ p.add_argument(
30
+ "--verbose",
31
+ "-v",
32
+ action="store_true",
33
+ help="Print a human-readable multi-line summary.",
34
+ )
35
+ p.add_argument(
36
+ "--min",
37
+ type=float,
38
+ default=None,
39
+ metavar="USD",
40
+ help="Exit 1 if balance is below this USD threshold.",
41
+ )
42
+ p.set_defaults(handler=_run)
43
+
44
+
45
+ def _run(args) -> int:
46
+ try:
47
+ from ..client import build_client_from_auth
48
+ client = build_client_from_auth()
49
+ except auth.AuthError as e:
50
+ print(str(e), file=sys.stderr)
51
+ return 2
52
+
53
+ try:
54
+ info = fetch_balance(client)
55
+ except VeniceAPIError as e:
56
+ print(f"balance: API error: {e}", file=sys.stderr)
57
+ if e.status == 401:
58
+ return 2
59
+ if e.status == 429:
60
+ return 4
61
+ return 5
62
+
63
+ if info is None:
64
+ print("balance: API returned no data block", file=sys.stderr)
65
+ return 5
66
+
67
+ usd = info.get("usd")
68
+ diem = info.get("diem")
69
+ total = info.get("total")
70
+ tier = info.get("tier")
71
+ next_epoch = info.get("next_epoch")
72
+ key_exp = info.get("key_expires")
73
+
74
+ if args.json:
75
+ json.dump(
76
+ {
77
+ "USD": usd,
78
+ "DIEM": diem,
79
+ "total_usd_equiv": total,
80
+ "tier": tier,
81
+ "next_epoch": next_epoch,
82
+ "key_expires": key_exp,
83
+ "spend_order": list(SPEND_ORDER),
84
+ "notes": {
85
+ "DIEM": "epoch allowance from staked DIEM; resets at next_epoch",
86
+ "BUNDLED_CREDITS": "monthly bundle; drains before USD cash; not exposed via inference key",
87
+ "VCU": "tier compute units; not exposed via inference key",
88
+ },
89
+ },
90
+ sys.stdout,
91
+ indent=2,
92
+ )
93
+ sys.stdout.write("\n")
94
+ elif args.verbose:
95
+ print(f"Tier: {tier or 'unknown'}")
96
+ print(f"Spendable: {format_balance_breakdown(info)}")
97
+ if diem is not None:
98
+ try:
99
+ print(
100
+ f" DIEM allow.: {float(diem):.4f} "
101
+ f"(this epoch; resets {next_epoch or '?'})"
102
+ )
103
+ except (TypeError, ValueError):
104
+ print(f" DIEM allow.: {diem}")
105
+ print(f" monthly: (not exposed via inference key; drains before cash)")
106
+ print(f" USD cash: {format_usd(usd)}")
107
+ print(f"Spend order: {' -> '.join(SPEND_ORDER)}")
108
+ print(f"Key expires: {key_exp or 'never'}")
109
+ else:
110
+ print(format_usd(total))
111
+
112
+ if args.min is not None and total is not None:
113
+ try:
114
+ if float(total) < float(args.min):
115
+ print(
116
+ f"balance: total {format_usd(total)} is below floor "
117
+ f"{format_usd(args.min)}",
118
+ file=sys.stderr,
119
+ )
120
+ return 1
121
+ except (TypeError, ValueError):
122
+ pass
123
+
124
+ return 0
@@ -0,0 +1,126 @@
1
+ """`venice bg-remove` -- strip an image's background via /image/background-remove.
2
+
3
+ Venice's generate call treats `background: transparent` as a no-op, so
4
+ transparent assets (e.g. rank insignia) are made opaque then run through this
5
+ endpoint, which returns a PNG with a transparent background. The source is
6
+ either a local file (sent as base64 in a JSON body) or an image URL. The
7
+ response is raw image/png bytes, so we use the client's bytes-or-json path.
8
+ Pricing is dynamic ($0.001-$10.00/call); the balance is shown and you confirm.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+ from .. import auth
18
+ from ..client import build_client_from_auth
19
+ from ._shared import (
20
+ confirm_or_exit,
21
+ over_budget,
22
+ post_binary_op,
23
+ print_balance_and_remaining,
24
+ print_estimate,
25
+ resolve_output,
26
+ )
27
+
28
+ ENDPOINT = "/image/background-remove"
29
+ MAX_INPUT_BYTES = 25 * 1024 * 1024 # API limit: input file < 25 MB
30
+ URL_DEFAULT_NAME = "venice-nobg.png"
31
+
32
+
33
+ def register(subparsers) -> None:
34
+ p = subparsers.add_parser(
35
+ "bg-remove",
36
+ help="Remove an image's background via /image/background-remove.",
37
+ description=(
38
+ "Returns a PNG with a transparent background. Source is a local file "
39
+ "(positional) or --image-url. Use for assets that need alpha, e.g. "
40
+ "`venice bg-remove insignia.png`. Pricing is dynamic; the balance is "
41
+ "shown and you confirm before the charge."
42
+ ),
43
+ )
44
+ src = p.add_mutually_exclusive_group()
45
+ src.add_argument("input", type=Path, nargs="?", default=None,
46
+ help="Image file whose background to remove.")
47
+ src.add_argument("--image-url", default=None, metavar="URL",
48
+ help="Remove the background from an image at this URL instead.")
49
+ p.add_argument("--output", "-o", type=Path, default=None,
50
+ help="Output file or directory. Default: cwd/<input>-nobg.png "
51
+ f"(or {URL_DEFAULT_NAME} for --image-url).")
52
+ p.add_argument("--yes", "-y", action="store_true")
53
+ p.add_argument("--dry-run", action="store_true",
54
+ help="Show the planned output and exit; don't call the API.")
55
+ p.add_argument(
56
+ "--max-spend",
57
+ type=float,
58
+ default=None,
59
+ metavar="USD",
60
+ help="Refuse if the estimated cost exceeds this cap. Note: bg-remove "
61
+ "pricing is dynamic, so no pre-charge estimate is available.",
62
+ )
63
+ p.add_argument("--no-balance", action="store_true",
64
+ help="Skip the upfront balance display.")
65
+ p.set_defaults(handler=_run)
66
+
67
+
68
+ def _validate(args) -> Optional[int]:
69
+ if (args.input is None) == (args.image_url is None):
70
+ print("bg-remove: provide exactly one of INPUT file or --image-url",
71
+ file=sys.stderr)
72
+ return 2
73
+ if args.input is not None:
74
+ inp = args.input
75
+ if not inp.is_file():
76
+ print(f"bg-remove: input file not found: {inp}", file=sys.stderr)
77
+ return 2
78
+ size = inp.stat().st_size
79
+ if size == 0:
80
+ print(f"bg-remove: input {inp} is empty", file=sys.stderr)
81
+ return 2
82
+ if size > MAX_INPUT_BYTES:
83
+ print(f"bg-remove: input {inp} is {size} bytes; must be < 25 MB",
84
+ file=sys.stderr)
85
+ return 2
86
+ return None
87
+
88
+
89
+ def _build_body(args) -> dict:
90
+ if args.input is not None:
91
+ b64 = base64.b64encode(args.input.read_bytes()).decode("ascii")
92
+ return {"image": b64}
93
+ return {"image_url": args.image_url}
94
+
95
+
96
+ def _run(args) -> int:
97
+ rc = _validate(args)
98
+ if rc is not None:
99
+ return rc
100
+
101
+ try:
102
+ client = build_client_from_auth()
103
+ except auth.AuthError as e:
104
+ print(str(e), file=sys.stderr)
105
+ return 2
106
+
107
+ cost = None # dynamic pricing -- no reliable upfront quote
108
+ print_estimate(cost, "background removal; dynamic $0.001-$10.00/call")
109
+ print_balance_and_remaining(client, cost, show=not args.no_balance)
110
+ if over_budget(cost, args.max_spend): # no-op while cost is unknown
111
+ return 1
112
+
113
+ default_name = (
114
+ f"{args.input.stem}-nobg.png" if args.input is not None else URL_DEFAULT_NAME
115
+ )
116
+ out_path = resolve_output(args.output, default_name)
117
+ if args.dry_run:
118
+ print(f"would write: {out_path.resolve()}", file=sys.stderr)
119
+ return 0
120
+
121
+ rc = confirm_or_exit(args.yes)
122
+ if rc is not None:
123
+ return rc
124
+
125
+ body = _build_body(args)
126
+ return post_binary_op(client, ENDPOINT, body, out_path, "bg-remove")
@@ -0,0 +1,263 @@
1
+ """`venice chat` -- one-shot /chat/completions.
2
+
3
+ Built on the official `openai` SDK (Venice is OpenAI-compatible; the SDK is
4
+ lazy-imported so the rest of the stdlib-only CLI works without it). Venice's
5
+ own chat extensions -- web search, characters, thinking control, etc. -- are
6
+ surfaced as flags and passed through `extra_body={"venice_parameters": ...}`.
7
+
8
+ The free `/models?type=text` catalog GET (via the lean urllib client) is used to
9
+ validate `--model` and resolve a default before the paid completion call --
10
+ mirrors the guard pattern in `venice music`.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import sys
16
+ from typing import Optional
17
+
18
+ from .. import auth
19
+ from ..client import build_client_from_auth
20
+ from . import _models, _openai
21
+
22
+
23
+ def register(subparsers) -> None:
24
+ p = subparsers.add_parser(
25
+ "chat",
26
+ help="One-shot chat completion (/chat/completions).",
27
+ description=(
28
+ "Send a single message to a Venice text model and print the reply. "
29
+ "Reads the message from the argument, or from stdin when it is '-' "
30
+ "or piped. Supports Venice extensions: web search, characters, and "
31
+ "reasoning-model thinking control."
32
+ ),
33
+ )
34
+ p.add_argument(
35
+ "message",
36
+ nargs="?",
37
+ help="User message. Use '-' (or pipe stdin) to read from stdin.",
38
+ )
39
+ p.add_argument("--system", "-s", default=None, help="Optional system prompt.")
40
+ p.add_argument(
41
+ "--model",
42
+ "-m",
43
+ default=None,
44
+ help="Text model id (default: the catalog's 'default'-trait model).",
45
+ )
46
+ p.add_argument("--temperature", "-t", type=float, default=None)
47
+ p.add_argument("--max-tokens", type=int, default=None, dest="max_tokens")
48
+
49
+ stream_grp = p.add_mutually_exclusive_group()
50
+ stream_grp.add_argument(
51
+ "--stream", dest="stream", action="store_true", default=True,
52
+ help="Stream the reply incrementally (default).",
53
+ )
54
+ stream_grp.add_argument(
55
+ "--no-stream", dest="stream", action="store_false",
56
+ help="Wait for the full reply, then print it.",
57
+ )
58
+ p.add_argument(
59
+ "--json", action="store_true",
60
+ help="Print the raw response object (forces --no-stream).",
61
+ )
62
+
63
+ # --- Venice extensions -> venice_parameters ---
64
+ ext = p.add_argument_group("Venice extensions")
65
+ ext.add_argument(
66
+ "--web-search", choices=("auto", "on", "off"), default=None,
67
+ dest="web_search", help="Enable Venice web search (default: off).",
68
+ )
69
+ ext.add_argument(
70
+ "--web-citations", action="store_true", dest="web_citations",
71
+ help="Ask the model to cite web sources (with --web-search).",
72
+ )
73
+ ext.add_argument(
74
+ "--web-scraping", action="store_true", dest="web_scraping",
75
+ help="Scrape URLs in the message via Firecrawl.",
76
+ )
77
+ ext.add_argument(
78
+ "--character", default=None, metavar="SLUG",
79
+ help="Use a public Venice character (its Public ID slug).",
80
+ )
81
+ ext.add_argument(
82
+ "--no-venice-system-prompt", action="store_true",
83
+ dest="no_venice_system_prompt",
84
+ help="Omit Venice's supplied system prompt (default: included).",
85
+ )
86
+ ext.add_argument(
87
+ "--strip-thinking", action="store_true", dest="strip_thinking",
88
+ help="Strip <think> blocks from reasoning-model output.",
89
+ )
90
+ ext.add_argument(
91
+ "--no-thinking", action="store_true", dest="no_thinking",
92
+ help="Disable thinking on supported reasoning models.",
93
+ )
94
+ ext.add_argument(
95
+ "--x-search", action="store_true", dest="x_search",
96
+ help="Enable xAI web+X search (extra ~$0.01/search; grok models).",
97
+ )
98
+ p.set_defaults(handler=_run)
99
+
100
+
101
+ def _resolve_message(args) -> Optional[str]:
102
+ """Positional message, or stdin when '-' / piped. None if nothing given."""
103
+ msg = args.message
104
+ if msg == "-" or (msg is None and not sys.stdin.isatty()):
105
+ data = sys.stdin.read().strip()
106
+ return data or None
107
+ return msg
108
+
109
+
110
+ def _venice_parameters(args) -> dict:
111
+ """Assemble venice_parameters from the extension flags (only set keys)."""
112
+ vp: dict = {}
113
+ if args.web_search is not None:
114
+ vp["enable_web_search"] = args.web_search
115
+ if args.web_citations:
116
+ vp["enable_web_citations"] = True
117
+ if args.web_scraping:
118
+ vp["enable_web_scraping"] = True
119
+ if args.character:
120
+ vp["character_slug"] = args.character
121
+ if args.no_venice_system_prompt:
122
+ vp["include_venice_system_prompt"] = False
123
+ if args.strip_thinking:
124
+ vp["strip_thinking_response"] = True
125
+ if args.no_thinking:
126
+ vp["disable_thinking"] = True
127
+ if args.x_search:
128
+ vp["enable_x_search"] = True
129
+ return vp
130
+
131
+
132
+ def _build_kwargs(args, model: str, message: str) -> dict:
133
+ messages = []
134
+ if args.system:
135
+ messages.append({"role": "system", "content": args.system})
136
+ messages.append({"role": "user", "content": message})
137
+
138
+ kwargs: dict = {"model": model, "messages": messages}
139
+ if args.temperature is not None:
140
+ kwargs["temperature"] = args.temperature
141
+ if args.max_tokens is not None:
142
+ kwargs["max_tokens"] = args.max_tokens
143
+ vp = _venice_parameters(args)
144
+ if vp:
145
+ kwargs["extra_body"] = {"venice_parameters": vp}
146
+ return kwargs
147
+
148
+
149
+ def _as_dict(value) -> Optional[dict]:
150
+ if value is None:
151
+ return None
152
+ if hasattr(value, "model_dump"):
153
+ return value.model_dump()
154
+ if isinstance(value, dict):
155
+ return value
156
+ return None
157
+
158
+
159
+ def _print_citations(venice_params) -> None:
160
+ vp = _as_dict(venice_params)
161
+ if not vp:
162
+ return
163
+ cites = vp.get("web_search_citations")
164
+ if not isinstance(cites, list) or not cites:
165
+ return
166
+ print("\nSources:", file=sys.stderr)
167
+ for i, c in enumerate(cites, 1):
168
+ cd = _as_dict(c) or {}
169
+ title = cd.get("title", "")
170
+ url = cd.get("url", "")
171
+ date = cd.get("date")
172
+ line = f" [{i}] {title} -- {url}"
173
+ if date:
174
+ line += f" ({date})"
175
+ print(line, file=sys.stderr)
176
+
177
+
178
+ def _print_usage(usage) -> None:
179
+ u = _as_dict(usage)
180
+ if not u:
181
+ return
182
+ pt = u.get("prompt_tokens")
183
+ ct = u.get("completion_tokens")
184
+ tt = u.get("total_tokens")
185
+ if tt is not None:
186
+ print(f"usage: prompt={pt} completion={ct} total={tt}", file=sys.stderr)
187
+
188
+
189
+ def _run(args) -> int:
190
+ message = _resolve_message(args)
191
+ if not message:
192
+ print("chat: no message (pass an argument or pipe stdin)", file=sys.stderr)
193
+ return 2
194
+
195
+ openai = _openai.import_openai("chat")
196
+ if openai is None:
197
+ return 2
198
+
199
+ try:
200
+ client = build_client_from_auth()
201
+ except auth.AuthError as e:
202
+ print(str(e), file=sys.stderr)
203
+ return 2
204
+
205
+ models = _models.catalog(client, "text")
206
+ model, rc = _models.resolve_model(
207
+ args.model, models, label="chat", noun="text model"
208
+ )
209
+ if rc is not None:
210
+ return rc
211
+
212
+ oai = _openai.build_openai(openai, client)
213
+ kwargs = _build_kwargs(args, model, message)
214
+
215
+ stream = args.stream and not args.json
216
+ try:
217
+ if stream:
218
+ return _run_stream(oai, kwargs)
219
+ return _run_once(oai, kwargs, args.json)
220
+ except openai.OpenAIError as e:
221
+ return _openai.status_to_exit(openai, e, "chat")
222
+
223
+
224
+ def _run_once(oai, kwargs: dict, as_json: bool) -> int:
225
+ resp = oai.chat.completions.create(**kwargs)
226
+ if as_json:
227
+ json.dump(resp.model_dump(), sys.stdout, indent=2, default=str)
228
+ sys.stdout.write("\n")
229
+ return 0
230
+ content = ""
231
+ if resp.choices:
232
+ content = resp.choices[0].message.content or ""
233
+ print(content)
234
+ _print_citations(getattr(resp, "venice_parameters", None))
235
+ return 0
236
+
237
+
238
+ def _run_stream(oai, kwargs: dict) -> int:
239
+ kwargs = dict(kwargs)
240
+ kwargs["stream"] = True
241
+ kwargs["stream_options"] = {"include_usage": True}
242
+ stream = oai.chat.completions.create(**kwargs)
243
+
244
+ citations = None
245
+ usage = None
246
+ wrote_any = False
247
+ for chunk in stream:
248
+ vp = getattr(chunk, "venice_parameters", None)
249
+ if vp is not None and citations is None:
250
+ citations = vp
251
+ if getattr(chunk, "usage", None):
252
+ usage = chunk.usage
253
+ if chunk.choices:
254
+ piece = getattr(chunk.choices[0].delta, "content", None)
255
+ if piece:
256
+ sys.stdout.write(piece)
257
+ sys.stdout.flush()
258
+ wrote_any = True
259
+ if wrote_any:
260
+ sys.stdout.write("\n")
261
+ _print_citations(citations)
262
+ _print_usage(usage)
263
+ return 0