impreza-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.
- impreza_cli/__init__.py +14 -0
- impreza_cli/commands/__init__.py +7 -0
- impreza_cli/commands/_helpers.py +128 -0
- impreza_cli/commands/account.py +576 -0
- impreza_cli/commands/catalog.py +270 -0
- impreza_cli/commands/context.py +232 -0
- impreza_cli/commands/doctor.py +427 -0
- impreza_cli/commands/domain.py +858 -0
- impreza_cli/commands/invoice.py +198 -0
- impreza_cli/commands/key.py +104 -0
- impreza_cli/commands/orders.py +478 -0
- impreza_cli/commands/services.py +100 -0
- impreza_cli/commands/vps.py +812 -0
- impreza_cli/commands/vps_cloud.py +865 -0
- impreza_cli/commands/vps_proxmox.py +727 -0
- impreza_cli/commands/webhooks.py +483 -0
- impreza_cli/config.py +405 -0
- impreza_cli/main.py +100 -0
- impreza_cli/output.py +207 -0
- impreza_cli/sdk.py +97 -0
- impreza_cli/state.py +94 -0
- impreza_cli-0.3.0.dist-info/METADATA +296 -0
- impreza_cli-0.3.0.dist-info/RECORD +26 -0
- impreza_cli-0.3.0.dist-info/WHEEL +5 -0
- impreza_cli-0.3.0.dist-info/entry_points.txt +2 -0
- impreza_cli-0.3.0.dist-info/top_level.txt +1 -0
impreza_cli/output.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Output formatting for CLI commands.
|
|
2
|
+
|
|
3
|
+
The CLI supports three output modes selected via the global
|
|
4
|
+
``--output`` flag (default: ``table``):
|
|
5
|
+
|
|
6
|
+
* ``table`` — Rich-rendered tables for human consumption (default).
|
|
7
|
+
* ``json`` — UTF-8 JSON, pretty-printed with 2-space indent. Pipes
|
|
8
|
+
cleanly into ``jq`` and friends.
|
|
9
|
+
* ``yaml`` — defer-imported PyYAML output (only loaded when actually
|
|
10
|
+
selected so the import cost is paid only on use).
|
|
11
|
+
|
|
12
|
+
Phase 2.1 ships only ``table`` and ``json`` since the context
|
|
13
|
+
commands' output is small. ``yaml`` lands in Phase 2.7 alongside the
|
|
14
|
+
output polish + tab completion pass; the ``OutputFormat`` enum
|
|
15
|
+
already lists it so command code can target the final API today and
|
|
16
|
+
the 2.7 work is purely additive.
|
|
17
|
+
|
|
18
|
+
Stderr is a separate channel via :func:`error` — used for
|
|
19
|
+
user-visible errors (typed config failures, etc.) so the success
|
|
20
|
+
output on stdout stays scriptable.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import sys
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import typer
|
|
31
|
+
from rich.console import Console
|
|
32
|
+
from rich.table import Table
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"OutputFormat",
|
|
36
|
+
"error",
|
|
37
|
+
"info",
|
|
38
|
+
"print_dict",
|
|
39
|
+
"print_table",
|
|
40
|
+
"success",
|
|
41
|
+
"warning",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OutputFormat(str, Enum):
|
|
46
|
+
"""Selectable output mode. ``str``-mixin so it round-trips
|
|
47
|
+
cleanly through Typer's `--output` flag."""
|
|
48
|
+
|
|
49
|
+
TABLE = "table"
|
|
50
|
+
JSON = "json"
|
|
51
|
+
YAML = "yaml"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_stdout = Console()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def error(message: str) -> None:
|
|
58
|
+
"""Print a user-facing error message to stderr in red.
|
|
59
|
+
|
|
60
|
+
Uses ``typer.secho`` (line-based, via Click) rather than Rich
|
|
61
|
+
so the output doesn't wrap based on detected terminal width.
|
|
62
|
+
Wrapping was hiding the back half of long error messages
|
|
63
|
+
(e.g. ``[request_id=...]`` suffixes) from stderr capture in
|
|
64
|
+
CliRunner-driven tests, and would do the same to grep-piped
|
|
65
|
+
output on narrow real terminals.
|
|
66
|
+
|
|
67
|
+
Bugs should still raise so the traceback isn't swallowed —
|
|
68
|
+
this helper is only for friendly errors on expected failures.
|
|
69
|
+
"""
|
|
70
|
+
typer.secho(f"Error: {message}", err=True, fg=typer.colors.RED, bold=True)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def success(message: str) -> None:
|
|
74
|
+
"""Print a user-facing success message to stdout in green.
|
|
75
|
+
|
|
76
|
+
Companion to :func:`error`. Use for the trailing "X created /
|
|
77
|
+
deleted / updated" line that confirms an operation took effect.
|
|
78
|
+
No "OK:" prefix — Phase 1.6's cp1252 lesson kept us off Unicode
|
|
79
|
+
glyphs and the colour itself reads as success cue without one.
|
|
80
|
+
|
|
81
|
+
Note: success messages go to stdout (not stderr) so they don't
|
|
82
|
+
confuse scripts that pipe stderr to /dev/null while parsing
|
|
83
|
+
stdout. Tests asserting against output should look at
|
|
84
|
+
``result.stdout``.
|
|
85
|
+
"""
|
|
86
|
+
typer.secho(message, fg=typer.colors.GREEN, bold=True)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def info(message: str) -> None:
|
|
90
|
+
"""Print a user-facing informational message to stdout in cyan.
|
|
91
|
+
|
|
92
|
+
Use for hints / status notes that aren't success per se —
|
|
93
|
+
"Reboot to apply", "Operation queued — uuid X", etc. The cyan
|
|
94
|
+
is distinct enough from green-success that a script-reading
|
|
95
|
+
user can tell at a glance whether something completed or is
|
|
96
|
+
waiting on an action.
|
|
97
|
+
"""
|
|
98
|
+
typer.secho(message, fg=typer.colors.CYAN)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def warning(message: str) -> None:
|
|
102
|
+
"""Print a user-facing warning to stderr in yellow.
|
|
103
|
+
|
|
104
|
+
Goes to stderr (not stdout) so scripts capturing stdout for
|
|
105
|
+
parsing don't see the warning in their data stream. Use for
|
|
106
|
+
"the call succeeded but something looks off" cases —
|
|
107
|
+
deprecation hints, surprising upstream behaviour, edge
|
|
108
|
+
conditions that don't actually fail.
|
|
109
|
+
"""
|
|
110
|
+
typer.secho(f"Warning: {message}", err=True, fg=typer.colors.YELLOW)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def print_table(
|
|
114
|
+
title: str,
|
|
115
|
+
rows: list[dict[str, Any]],
|
|
116
|
+
*,
|
|
117
|
+
columns: list[str] | None = None,
|
|
118
|
+
fmt: OutputFormat = OutputFormat.TABLE,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Render ``rows`` (list of homogenous dicts) per the chosen format.
|
|
121
|
+
|
|
122
|
+
``columns`` overrides the column order. When omitted, columns are
|
|
123
|
+
taken from the first row's key order — Python 3.7+ preserves
|
|
124
|
+
dict insertion order so this is deterministic per the calling
|
|
125
|
+
code.
|
|
126
|
+
|
|
127
|
+
Empty ``rows`` is acceptable; we render an empty table with the
|
|
128
|
+
headers (or print ``[]`` for JSON).
|
|
129
|
+
"""
|
|
130
|
+
if fmt is OutputFormat.JSON:
|
|
131
|
+
sys.stdout.write(json.dumps(rows, indent=2, ensure_ascii=False))
|
|
132
|
+
sys.stdout.write("\n")
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if fmt is OutputFormat.YAML:
|
|
136
|
+
_yaml_dump(rows)
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# Table rendering.
|
|
140
|
+
if columns is None:
|
|
141
|
+
columns = list(rows[0].keys()) if rows else []
|
|
142
|
+
table = Table(title=title, header_style="bold cyan", show_lines=False)
|
|
143
|
+
for col in columns:
|
|
144
|
+
table.add_column(col)
|
|
145
|
+
for row in rows:
|
|
146
|
+
table.add_row(*[_render_cell(row.get(col)) for col in columns])
|
|
147
|
+
_stdout.print(table)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def print_dict(
|
|
151
|
+
title: str,
|
|
152
|
+
data: dict[str, Any],
|
|
153
|
+
*,
|
|
154
|
+
fmt: OutputFormat = OutputFormat.TABLE,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Render a single resource (dict) — two-column ``Field / Value``
|
|
157
|
+
table by default."""
|
|
158
|
+
if fmt is OutputFormat.JSON:
|
|
159
|
+
sys.stdout.write(json.dumps(data, indent=2, ensure_ascii=False))
|
|
160
|
+
sys.stdout.write("\n")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
if fmt is OutputFormat.YAML:
|
|
164
|
+
_yaml_dump(data)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
table = Table(title=title, header_style="bold cyan", show_header=True)
|
|
168
|
+
table.add_column("Field")
|
|
169
|
+
table.add_column("Value")
|
|
170
|
+
for k, v in data.items():
|
|
171
|
+
table.add_row(str(k), _render_cell(v))
|
|
172
|
+
_stdout.print(table)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _render_cell(value: Any) -> str:
|
|
176
|
+
"""Format a single cell for table rendering.
|
|
177
|
+
|
|
178
|
+
Uses ASCII glyphs (``-``, ``yes`` / ``no``) rather than fancy
|
|
179
|
+
Unicode (``—`` / ``✓`` / ``✗``) because Windows legacy consoles
|
|
180
|
+
default to cp1252 and crash on the latter — Phase 1.6's smoke
|
|
181
|
+
learned this the hard way and we want CLI output to read on
|
|
182
|
+
every terminal, not just modern UTF-8 ones.
|
|
183
|
+
"""
|
|
184
|
+
if value is None:
|
|
185
|
+
return "[dim]-[/]"
|
|
186
|
+
if isinstance(value, bool):
|
|
187
|
+
return "yes" if value else "no"
|
|
188
|
+
return str(value)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _yaml_dump(data: Any) -> None:
|
|
192
|
+
"""Defer-import PyYAML so installs that never touch YAML output
|
|
193
|
+
don't pay the dependency cost.
|
|
194
|
+
|
|
195
|
+
Phase 2.7 wired pyyaml as the optional ``[yaml]`` extra. Calling
|
|
196
|
+
this function on an install that didn't pull in that extra
|
|
197
|
+
surfaces a clear ImportError telling the user how to upgrade or
|
|
198
|
+
pick a different format.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
import yaml
|
|
202
|
+
except ImportError as exc:
|
|
203
|
+
raise RuntimeError(
|
|
204
|
+
"YAML output requires the optional `pyyaml` dependency. "
|
|
205
|
+
"Install with: pip install impreza-cli[yaml]"
|
|
206
|
+
) from exc
|
|
207
|
+
sys.stdout.write(yaml.safe_dump(data, sort_keys=False, allow_unicode=True))
|
impreza_cli/sdk.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""SDK bootstrap — single source of truth for "given a context, hand
|
|
2
|
+
me an :class:`impreza.Client`".
|
|
3
|
+
|
|
4
|
+
Every command that needs to call the API goes through this module so
|
|
5
|
+
the resolution rules (which context, which base URL, Tor or not)
|
|
6
|
+
live in one place. Adding new resolution sources later (per-context
|
|
7
|
+
``proxy=`` overrides, env-var fallbacks, etc.) is a one-file change.
|
|
8
|
+
|
|
9
|
+
The SDK's own :func:`Client.from_env` reads ``IMPREZA_API_KEY`` /
|
|
10
|
+
``IMPREZA_API_SECRET`` directly from the environment — that path
|
|
11
|
+
stays available for users who prefer env-var auth and bypass the
|
|
12
|
+
context machinery entirely. This module is for the context-driven
|
|
13
|
+
path (the CLI's primary UX).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
from impreza import Client
|
|
23
|
+
|
|
24
|
+
from .config import Config, ConfigError
|
|
25
|
+
from .output import error
|
|
26
|
+
from .state import GlobalState
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"make_client",
|
|
30
|
+
"make_client_or_exit",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def make_client(
|
|
35
|
+
state: GlobalState,
|
|
36
|
+
*,
|
|
37
|
+
config_path: Path | None = None,
|
|
38
|
+
) -> Client:
|
|
39
|
+
"""Build a sync :class:`impreza.Client` from the active context.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
state: The CLI's :class:`~.state.GlobalState`. Reads
|
|
43
|
+
``state.context_override`` to pick a non-default context.
|
|
44
|
+
config_path: Optional override for the config file location
|
|
45
|
+
(mostly used in tests; production flow always uses the
|
|
46
|
+
default).
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ConfigError: any of the config-resolution errors —
|
|
50
|
+
:class:`~.config.NoContextsConfigured`,
|
|
51
|
+
:class:`~.config.NoActiveContext`,
|
|
52
|
+
:class:`~.config.ContextNotFound`. The :func:`make_client_or_exit`
|
|
53
|
+
wrapper catches these for the typical CLI command flow;
|
|
54
|
+
tests / library callers can let them propagate.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A sync :class:`Client` ready to call. Caller is responsible
|
|
58
|
+
for closing it (the standard `with Client(...) as c:` pattern
|
|
59
|
+
works — Typer commands just let it close on process exit
|
|
60
|
+
which is fine for short-lived CLI invocations).
|
|
61
|
+
"""
|
|
62
|
+
cfg = Config.load(config_path)
|
|
63
|
+
ctx = cfg.get_context(state.context_override)
|
|
64
|
+
|
|
65
|
+
kwargs: dict[str, Any] = {
|
|
66
|
+
"api_key": ctx.api_key,
|
|
67
|
+
"api_secret": ctx.api_secret,
|
|
68
|
+
}
|
|
69
|
+
if ctx.base_url:
|
|
70
|
+
kwargs["base_url"] = ctx.base_url
|
|
71
|
+
|
|
72
|
+
# Tor preference comes from the [settings] block (CLI-wide). A
|
|
73
|
+
# future per-context override (one VPN context, one clearnet, etc.)
|
|
74
|
+
# would slot in here without touching callers.
|
|
75
|
+
if cfg.settings.use_tor:
|
|
76
|
+
kwargs["use_tor"] = True
|
|
77
|
+
|
|
78
|
+
return Client(**kwargs)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def make_client_or_exit(
|
|
82
|
+
state: GlobalState,
|
|
83
|
+
*,
|
|
84
|
+
config_path: Path | None = None,
|
|
85
|
+
) -> Client:
|
|
86
|
+
"""Same as :func:`make_client`, but converts config errors into
|
|
87
|
+
friendly stderr output and a non-zero exit.
|
|
88
|
+
|
|
89
|
+
This is the path every Typer command follows. Bugs (network
|
|
90
|
+
errors at construction time, etc.) still raise so the traceback
|
|
91
|
+
isn't swallowed.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
return make_client(state, config_path=config_path)
|
|
95
|
+
except ConfigError as exc:
|
|
96
|
+
error(str(exc))
|
|
97
|
+
raise typer.Exit(code=1) from None
|
impreza_cli/state.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""CLI-wide shared state passed between the root callback and
|
|
2
|
+
subcommands via ``typer.Context.obj``.
|
|
3
|
+
|
|
4
|
+
Subcommands never read these flags from their own argument list —
|
|
5
|
+
they pull them off ``ctx.obj`` so the same flag can be set at the
|
|
6
|
+
``impreza`` root (`impreza --output json account info`) or at the
|
|
7
|
+
subcommand level (`impreza account info --output json`). Per-command
|
|
8
|
+
overrides win; falling back through to the context override and
|
|
9
|
+
then the table default.
|
|
10
|
+
|
|
11
|
+
The state object is intentionally tiny — only fields that are read
|
|
12
|
+
by more than one command should live here. Anything specific to a
|
|
13
|
+
single command stays as a regular function argument.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
from .output import OutputFormat
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class GlobalState:
|
|
27
|
+
"""Values populated by the root callback and shared with every
|
|
28
|
+
subcommand."""
|
|
29
|
+
|
|
30
|
+
#: When set, the named context is used instead of the default.
|
|
31
|
+
#: ``None`` means "use the config file's ``default_context``".
|
|
32
|
+
context_override: str | None = None
|
|
33
|
+
|
|
34
|
+
#: Default output format selected via the global ``--output`` flag.
|
|
35
|
+
#: Per-command ``--output`` flags can override this on a single
|
|
36
|
+
#: invocation; ``None`` here means "no preference, fall back to
|
|
37
|
+
#: the table default".
|
|
38
|
+
output: OutputFormat | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def from_typer_context(ctx: typer.Context) -> GlobalState:
|
|
42
|
+
"""Pull the :class:`GlobalState` off a Typer context.
|
|
43
|
+
|
|
44
|
+
Typer's :attr:`Context.obj` is typed as ``Any`` — this helper
|
|
45
|
+
narrows it back to ``GlobalState`` so callers don't have to
|
|
46
|
+
sprinkle ``cast()`` calls. If for any reason the obj is missing
|
|
47
|
+
(test invocation that bypasses the root callback, etc.), an
|
|
48
|
+
empty ``GlobalState`` is returned so commands don't NPE.
|
|
49
|
+
"""
|
|
50
|
+
obj = getattr(ctx, "obj", None)
|
|
51
|
+
if isinstance(obj, GlobalState):
|
|
52
|
+
return obj
|
|
53
|
+
return GlobalState()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resolve_output(
|
|
57
|
+
state: GlobalState,
|
|
58
|
+
per_command: OutputFormat | None,
|
|
59
|
+
) -> OutputFormat:
|
|
60
|
+
"""Pick the output format for a single command invocation.
|
|
61
|
+
|
|
62
|
+
Resolution order: per-command `--output` flag > global `--output`
|
|
63
|
+
flag > :attr:`OutputFormat.TABLE` default.
|
|
64
|
+
"""
|
|
65
|
+
if per_command is not None:
|
|
66
|
+
return per_command
|
|
67
|
+
if state.output is not None:
|
|
68
|
+
return state.output
|
|
69
|
+
return OutputFormat.TABLE
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def confirm_or_exit(message: str, *, yes: bool) -> None:
|
|
73
|
+
"""Prompt the user to confirm a destructive / costly action.
|
|
74
|
+
|
|
75
|
+
The standard pattern across every Phase 3 mutating command:
|
|
76
|
+
|
|
77
|
+
.. code-block:: python
|
|
78
|
+
|
|
79
|
+
confirm_or_exit("This will charge $12.99 from your balance.", yes=yes)
|
|
80
|
+
|
|
81
|
+
When ``yes=True`` the prompt is skipped (intent already opted-in
|
|
82
|
+
via the ``--yes`` flag). When the user declines, the CLI prints
|
|
83
|
+
"Cancelled." on stdout and exits 0 — declining is not an error.
|
|
84
|
+
|
|
85
|
+
The message should be a complete sentence describing what's
|
|
86
|
+
about to happen; the helper appends "Continue?" automatically.
|
|
87
|
+
"""
|
|
88
|
+
import typer # local import keeps state.py importable without typer
|
|
89
|
+
|
|
90
|
+
if yes:
|
|
91
|
+
return
|
|
92
|
+
if not typer.confirm(f"{message} Continue?", default=False):
|
|
93
|
+
typer.echo("Cancelled.")
|
|
94
|
+
raise typer.Exit(code=0)
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: impreza-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Official command-line interface for the Impreza Host public REST API
|
|
5
|
+
Author-email: Impreza Host <support@imprezahost.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://imprezahost.com
|
|
8
|
+
Project-URL: Documentation, https://docs.imprezahost.com
|
|
9
|
+
Project-URL: Repository, https://github.com/imprezahost/impreza-devkit
|
|
10
|
+
Project-URL: Changelog, https://github.com/imprezahost/impreza-devkit/blob/master/CHANGELOG.md
|
|
11
|
+
Keywords: impreza,hosting,cli,offshore,crypto
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: impreza-sdk
|
|
27
|
+
Requires-Dist: typer>=0.12
|
|
28
|
+
Requires-Dist: rich>=13.7
|
|
29
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
30
|
+
Requires-Dist: tomli-w>=1.0
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-cov>=4.1; extra == "test"
|
|
34
|
+
Requires-Dist: pyyaml>=6.0; extra == "test"
|
|
35
|
+
Requires-Dist: types-PyYAML>=6.0; extra == "test"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
38
|
+
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
39
|
+
Provides-Extra: yaml
|
|
40
|
+
Requires-Dist: pyyaml>=6.0; extra == "yaml"
|
|
41
|
+
|
|
42
|
+
# `impreza-cli` — Official CLI for Impreza Host
|
|
43
|
+
|
|
44
|
+
Command-line interface for the Impreza Host public REST API.
|
|
45
|
+
Built on top of [`impreza-sdk`](../sdk-python/README.md) — same
|
|
46
|
+
auth model, same Tor support, same retry behaviour, plus
|
|
47
|
+
multi-context configuration and Rich-rendered tables for human-
|
|
48
|
+
friendly output.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install impreza-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requires Python 3.10+. See [`../CHANGELOG.md`](../CHANGELOG.md) for
|
|
55
|
+
release history.
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 1. Add a context with your API credentials. Generate keys in
|
|
61
|
+
# Impreza Account → API Keys; whitelist the calling
|
|
62
|
+
# machine's IP at the same screen.
|
|
63
|
+
$ impreza context create personal --key imp_... --secret ...
|
|
64
|
+
Context 'personal' created and set as default.
|
|
65
|
+
|
|
66
|
+
# 2. Confirm everything works. impreza doctor runs five sequenced
|
|
67
|
+
# health checks (config, API reachable, key status, IP
|
|
68
|
+
# whitelist, account profile) and exits 0 only if all pass.
|
|
69
|
+
$ impreza doctor
|
|
70
|
+
|
|
71
|
+
impreza doctor
|
|
72
|
+
----------------------------------------
|
|
73
|
+
[OK] active-context: Default context
|
|
74
|
+
[OK] api-reachable: GET /account/api-keys/self OK (142ms)
|
|
75
|
+
key prefix='imp_a1b2c3d4', label='devkit'
|
|
76
|
+
[OK] key-status: status='active'
|
|
77
|
+
[OK] ip-whitelist: request_ip 200.1.2.3 matches entry ('home')
|
|
78
|
+
[OK] account-profile: Jane Doe <jane@example.com>, balance 5.00 USD
|
|
79
|
+
registered 2024-01-15
|
|
80
|
+
----------------------------------------
|
|
81
|
+
All checks passed. 5/5.
|
|
82
|
+
|
|
83
|
+
# 3. Read commands span every resource group:
|
|
84
|
+
$ impreza account info # profile + balance
|
|
85
|
+
$ impreza vps list # across both backends
|
|
86
|
+
$ impreza domain check example.com mydomain.io
|
|
87
|
+
$ impreza catalog products --group "VPS"
|
|
88
|
+
|
|
89
|
+
# 4. Pipe into jq for scripting (every read verb supports --output
|
|
90
|
+
# json | yaml):
|
|
91
|
+
$ impreza invoice list --output json \
|
|
92
|
+
| jq '[.[] | select(.status == "Unpaid")] | length'
|
|
93
|
+
|
|
94
|
+
# 5. Write verbs are gated by confirm_or_exit so you don't lose
|
|
95
|
+
# data accidentally; pass --yes / -y to skip prompts in scripts:
|
|
96
|
+
$ impreza vps reboot 17988
|
|
97
|
+
$ impreza vps proxmox snapshots create 17988 pre-update
|
|
98
|
+
$ impreza domain dns add example.com --type A --name www --value 1.2.3.4
|
|
99
|
+
|
|
100
|
+
# 6. Crypto top-up. --browser opens the BTCPay invoice URL
|
|
101
|
+
# automatically; --wait polls until the gateway confirms
|
|
102
|
+
# (default 2h timeout matches server-side invoice expiry).
|
|
103
|
+
$ impreza account topup --amount 50 --method xmr --browser --wait
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Authentication
|
|
107
|
+
|
|
108
|
+
Two ways to authenticate. The CLI tries them in order:
|
|
109
|
+
|
|
110
|
+
1. **Context** (recommended) — `impreza context create <name>` stores
|
|
111
|
+
credentials in a config file; commands read them automatically.
|
|
112
|
+
Per-invocation override via `impreza --context other <command>`.
|
|
113
|
+
|
|
114
|
+
2. **Environment variables** — `IMPREZA_API_KEY` + `IMPREZA_API_SECRET`.
|
|
115
|
+
Useful in CI, but contexts are preferred for local work.
|
|
116
|
+
|
|
117
|
+
The config file lives at:
|
|
118
|
+
|
|
119
|
+
| OS | Path |
|
|
120
|
+
|---|---|
|
|
121
|
+
| Linux | `$XDG_CONFIG_HOME/impreza/config.toml` (default `~/.config/impreza/config.toml`) |
|
|
122
|
+
| macOS | `~/Library/Application Support/impreza/config.toml` |
|
|
123
|
+
| Windows | `%APPDATA%\impreza\config.toml` |
|
|
124
|
+
|
|
125
|
+
Override with `IMPREZA_CONFIG=/path/to/config.toml` for testing or
|
|
126
|
+
non-standard layouts.
|
|
127
|
+
|
|
128
|
+
On POSIX, the config file is `chmod 0o600` after every write so only
|
|
129
|
+
the owner can read the credentials. Windows ACLs are left to the OS
|
|
130
|
+
default.
|
|
131
|
+
|
|
132
|
+
## Commands
|
|
133
|
+
|
|
134
|
+
The CLI groups commands by resource. Run `impreza <group> --help`
|
|
135
|
+
to see the full subcommand list, or `impreza <group> <command>
|
|
136
|
+
--help` for option-level detail.
|
|
137
|
+
|
|
138
|
+
| Group | Verbs | Notes |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `context` | `create / use / list / current / delete` | Local credential management — never hits the network |
|
|
141
|
+
| `doctor` | (single command) | Health check — config + API reachable + key status + IP whitelist + account profile |
|
|
142
|
+
| `account` | `info / balance / services / topup / topup-status` | Profile + balance + services + crypto top-up |
|
|
143
|
+
| `catalog` | `products / product / product-groups / tlds` | Pre-purchase discovery |
|
|
144
|
+
| `domain` | `show / check / pricing / register / transfer / set-nameservers / lock / unlock / id-protection / raa-verify / gdpr-auth / transfer-approval` + `domain dns list / add / update / delete / activate` | Domain registrations + full DNS CRUD |
|
|
145
|
+
| `vps` | `list / show / status / start / stop / reboot / shutdown / set-hostname / set-password / reinstall / migrate / cancel` + `vps proxmox snapshots / backups / backup-schedules / network` + `vps cloud images / rescue / iso / rdns / ssh-keys / vnc / vnc-password / resize / boot-order / ipv6` | Cross-backend (Proxmox + Cloud) VPS with smart dispatch |
|
|
146
|
+
| `order` | `list / show / create / upgrade` | Submit / browse product orders |
|
|
147
|
+
| `service` | `cancel` | Submit cancellation request (any service) |
|
|
148
|
+
| `webhook` | `list / show / create / update / delete / rotate-secret / deliveries / event-types` | Webhook subscription management + delivery history |
|
|
149
|
+
| `invoice` | `list / show` | Invoices with line items + transactions |
|
|
150
|
+
| `key` | `whoami` | Active API key identity + IP whitelist |
|
|
151
|
+
|
|
152
|
+
**Conventions:**
|
|
153
|
+
|
|
154
|
+
- Destructive verbs prompt for confirmation; pass `--yes` / `-y`
|
|
155
|
+
to skip the prompt in scripts.
|
|
156
|
+
- Operation-returning verbs (`vps reinstall`, `vps migrate`,
|
|
157
|
+
`vps proxmox snapshots rollback`, `vps proxmox backups
|
|
158
|
+
create/restore`) accept `--wait` to block on the Proxmox queue,
|
|
159
|
+
with `--timeout` (default 600 s for fast ops, 1800 s for the
|
|
160
|
+
slower restores).
|
|
161
|
+
- Cost-incurring verbs (`domain register/transfer/id-protection`,
|
|
162
|
+
`order create/upgrade`, `account topup`) call out the
|
|
163
|
+
balance impact in the confirmation prompt; an
|
|
164
|
+
`InsufficientCredit` 402 surfaces with a hint pointing at
|
|
165
|
+
`impreza account topup`.
|
|
166
|
+
- Verbs that mutate a resource emit a green success line on
|
|
167
|
+
stdout; queued / reboot-required state changes emit a cyan
|
|
168
|
+
info line. Errors are red on stderr.
|
|
169
|
+
|
|
170
|
+
**Service termination policy:** `service cancel` / `vps cancel`
|
|
171
|
+
submit an `AddCancelRequest` — staff approves the actual
|
|
172
|
+
termination. There is no direct customer path to terminate a
|
|
173
|
+
service or remove a service suspension (suspension is
|
|
174
|
+
billing-state and is removed automatically when the overdue
|
|
175
|
+
invoice is paid, or manually by staff after an abuse hold is
|
|
176
|
+
resolved).
|
|
177
|
+
|
|
178
|
+
## Output formats
|
|
179
|
+
|
|
180
|
+
Every command supports `--output table|json|yaml` (short form `-o`).
|
|
181
|
+
|
|
182
|
+
| Format | Default | Best for |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `table` | yes | human reading at the terminal |
|
|
185
|
+
| `json` | | piping into `jq`, automation, scripting |
|
|
186
|
+
| `yaml` | | human-editable config snapshots, CI/CD pipelines |
|
|
187
|
+
|
|
188
|
+
YAML output requires the optional `pyyaml` dependency:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pip install impreza-cli[yaml]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The CLI raises a clear `RuntimeError` pointing at the install hint
|
|
195
|
+
if you select `--output yaml` without it.
|
|
196
|
+
|
|
197
|
+
The flag works at both the global level and per-command:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# Global default for the invocation
|
|
201
|
+
impreza --output json account info
|
|
202
|
+
|
|
203
|
+
# Per-command override (wins over global)
|
|
204
|
+
impreza --output yaml account info --output table
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Tab completion
|
|
208
|
+
|
|
209
|
+
Typer ships completion for `bash`, `zsh`, `fish`, and PowerShell
|
|
210
|
+
out of the box:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Install for the current shell (auto-detected)
|
|
214
|
+
impreza --install-completion
|
|
215
|
+
|
|
216
|
+
# Or explicitly
|
|
217
|
+
impreza --install-completion bash # / zsh / fish / powershell
|
|
218
|
+
|
|
219
|
+
# Inspect the script before installing
|
|
220
|
+
impreza --show-completion bash
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
After installing, restart the shell (or `source ~/.bashrc` /
|
|
224
|
+
equivalent) and `impreza <TAB>` should suggest resource groups,
|
|
225
|
+
`impreza account <TAB>` should suggest verbs, and so on.
|
|
226
|
+
|
|
227
|
+
## Tor
|
|
228
|
+
|
|
229
|
+
Inherited from the SDK. Three knobs:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Per-context override at create time
|
|
233
|
+
impreza context create offshore \
|
|
234
|
+
--key imp_... --secret ... \
|
|
235
|
+
# No --proxy flag yet; for now, set IMPREZA_USE_TOR before invoking
|
|
236
|
+
|
|
237
|
+
# Env var, picked up by the SDK transparently
|
|
238
|
+
IMPREZA_USE_TOR=1 impreza account info
|
|
239
|
+
|
|
240
|
+
# Programmatic via the SDK (Python users skip the CLI for this)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The SDK's `auto_tor=True` path (probe Tor, fall back to clearnet)
|
|
244
|
+
isn't surfaced through the CLI yet — coming in a future release
|
|
245
|
+
alongside the `--via-tor` shortcut.
|
|
246
|
+
|
|
247
|
+
## Error handling
|
|
248
|
+
|
|
249
|
+
The CLI maps SDK exceptions to friendly stderr messages and a
|
|
250
|
+
non-zero exit code, matching the format `ImprezaError.__str__`
|
|
251
|
+
produces:
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
Error: Invalid API credentials. (code=UNAUTHORIZED) [request_id=req_abc]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Tracebacks never leak from expected failures (auth errors, missing
|
|
258
|
+
contexts, 404s, 429s, etc.). Bugs in the CLI itself still raise so
|
|
259
|
+
the traceback isn't swallowed — that's intentional.
|
|
260
|
+
|
|
261
|
+
## Development
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
git clone https://github.com/imprezahost/impreza-devkit.git
|
|
265
|
+
cd impreza-devkit/cli-python
|
|
266
|
+
|
|
267
|
+
python -m venv .venv
|
|
268
|
+
# Linux/macOS: source .venv/bin/activate
|
|
269
|
+
# Windows PowerShell: .venv\Scripts\Activate.ps1
|
|
270
|
+
|
|
271
|
+
# Install editable + test/dev/yaml extras + the SDK as a path dep
|
|
272
|
+
pip install -e ../sdk-python -e ".[test,dev,yaml]"
|
|
273
|
+
|
|
274
|
+
pytest # unit + Typer-runner E2E
|
|
275
|
+
ruff check
|
|
276
|
+
mypy --strict impreza_cli
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
To run the live integration smokes (skipped silently without creds):
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
export IMPREZA_API_KEY="imp_..."
|
|
283
|
+
export IMPREZA_API_SECRET="..."
|
|
284
|
+
# Optional, for `impreza domain show / dns list`:
|
|
285
|
+
export IMPREZA_TEST_DOMAIN="<a domain on your account>"
|
|
286
|
+
|
|
287
|
+
pytest -v -s tests/
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The smokes exercise the same surface as the unit tests against the
|
|
291
|
+
real API, so they catch contract drift between the CLI and the
|
|
292
|
+
server.
|
|
293
|
+
|
|
294
|
+
## License
|
|
295
|
+
|
|
296
|
+
MIT. See [`../LICENSE`](../LICENSE) at the repository root.
|