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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""``impreza catalog`` subcommand surface — Phase 2.3.
|
|
2
|
+
|
|
3
|
+
Three verbs reading from :class:`impreza.resources.catalog.CatalogResource`:
|
|
4
|
+
|
|
5
|
+
* ``impreza catalog products [--group X] [--type T]``
|
|
6
|
+
* ``impreza catalog product-groups``
|
|
7
|
+
* ``impreza catalog tlds [--filter .com,.net,...]``
|
|
8
|
+
|
|
9
|
+
The catalog is reference data — values change only when staff edit
|
|
10
|
+
Impreza Account, not as a side effect of customer activity. These commands are
|
|
11
|
+
the discovery layer customers use before placing orders (Phase 3).
|
|
12
|
+
|
|
13
|
+
VPS-Cloud catalog (sizes / locations) is deliberately deferred —
|
|
14
|
+
the underlying Cloud backend endpoints exist but return a deeply-
|
|
15
|
+
nested provider-specific shape that warrants its own focused pass
|
|
16
|
+
once Phase 3 ordering creates real demand for it.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import typer
|
|
24
|
+
from impreza.exceptions import ApiError
|
|
25
|
+
from impreza.models.product import Product
|
|
26
|
+
from impreza.models.tld import TldPricing
|
|
27
|
+
|
|
28
|
+
from ..output import OutputFormat, print_table
|
|
29
|
+
from ..sdk import make_client_or_exit
|
|
30
|
+
from ..state import from_typer_context, resolve_output
|
|
31
|
+
from ._helpers import exit_on_api_error as _exit_on_api_error
|
|
32
|
+
|
|
33
|
+
app = typer.Typer(
|
|
34
|
+
name="catalog",
|
|
35
|
+
help="Browse the product catalog, product groups, and TLD pricing.",
|
|
36
|
+
no_args_is_help=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── catalog products ─────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _cheapest_cycle(product: Product) -> str:
|
|
44
|
+
"""Format the cheapest billing cycle as a single-cell table value
|
|
45
|
+
(``monthly: 5.00``). Returns ``"-"`` for products with no
|
|
46
|
+
pricing configured (free or quote-only)."""
|
|
47
|
+
if not product.pricing:
|
|
48
|
+
return "-"
|
|
49
|
+
cycle, price = min(
|
|
50
|
+
product.pricing.items(),
|
|
51
|
+
key=lambda kv: kv[1].price,
|
|
52
|
+
)
|
|
53
|
+
return f"{cycle}: {price.price:.2f}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command("products")
|
|
57
|
+
def products(
|
|
58
|
+
typer_ctx: typer.Context,
|
|
59
|
+
group: str | None = typer.Option(
|
|
60
|
+
None,
|
|
61
|
+
"--group",
|
|
62
|
+
"-g",
|
|
63
|
+
help="Filter by product group name (case-insensitive substring match upstream).",
|
|
64
|
+
),
|
|
65
|
+
type_: str | None = typer.Option(
|
|
66
|
+
None,
|
|
67
|
+
"--type",
|
|
68
|
+
"-t",
|
|
69
|
+
help=(
|
|
70
|
+
"Filter by product type. One of: hostingaccount, "
|
|
71
|
+
"reselleraccount, server, other."
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
output: OutputFormat | None = typer.Option(
|
|
75
|
+
None,
|
|
76
|
+
"--output",
|
|
77
|
+
"-o",
|
|
78
|
+
help="Output format. Overrides the global --output flag.",
|
|
79
|
+
case_sensitive=False,
|
|
80
|
+
),
|
|
81
|
+
) -> None:
|
|
82
|
+
"""List products in the catalog with their cheapest cycle price.
|
|
83
|
+
|
|
84
|
+
Wraps ``GET /products``. Table mode shows id / name / group /
|
|
85
|
+
type / currency / cheapest-cycle. JSON / YAML modes emit the full
|
|
86
|
+
Product model (including the per-cycle pricing dict) so callers
|
|
87
|
+
can pick out a specific cycle for ordering.
|
|
88
|
+
"""
|
|
89
|
+
state = from_typer_context(typer_ctx)
|
|
90
|
+
fmt = resolve_output(state, output)
|
|
91
|
+
|
|
92
|
+
with make_client_or_exit(state) as client:
|
|
93
|
+
try:
|
|
94
|
+
items = client.catalog.products(group=group, type=type_)
|
|
95
|
+
except ApiError as exc:
|
|
96
|
+
_exit_on_api_error(exc)
|
|
97
|
+
|
|
98
|
+
if not items:
|
|
99
|
+
if group or type_:
|
|
100
|
+
filters = []
|
|
101
|
+
if group:
|
|
102
|
+
filters.append(f"group={group!r}")
|
|
103
|
+
if type_:
|
|
104
|
+
filters.append(f"type={type_!r}")
|
|
105
|
+
typer.echo(f"No products match the filter: {', '.join(filters)}.")
|
|
106
|
+
else:
|
|
107
|
+
typer.echo("No products in the catalog yet.")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if fmt is OutputFormat.TABLE:
|
|
111
|
+
rows: list[dict[str, Any]] = [
|
|
112
|
+
{
|
|
113
|
+
"id": p.id,
|
|
114
|
+
"name": p.name,
|
|
115
|
+
"group": p.group,
|
|
116
|
+
"type": p.type,
|
|
117
|
+
"currency": p.currency,
|
|
118
|
+
"cheapest": _cheapest_cycle(p),
|
|
119
|
+
}
|
|
120
|
+
for p in items
|
|
121
|
+
]
|
|
122
|
+
print_table(
|
|
123
|
+
f"Products ({len(rows)})",
|
|
124
|
+
rows,
|
|
125
|
+
columns=["id", "name", "group", "type", "currency", "cheapest"],
|
|
126
|
+
fmt=fmt,
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
# JSON / YAML: emit the full model — by_alias=False, so callers
|
|
130
|
+
# see Python attribute names (`register_prices` etc.). The
|
|
131
|
+
# Pydantic dump preserves the per-cycle pricing dict so callers
|
|
132
|
+
# can pick a specific cycle for ordering.
|
|
133
|
+
rows = [p.model_dump() for p in items]
|
|
134
|
+
print_table("Products", rows, fmt=fmt)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ── catalog product-groups ───────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("product-groups")
|
|
141
|
+
def product_groups(
|
|
142
|
+
typer_ctx: typer.Context,
|
|
143
|
+
output: OutputFormat | None = typer.Option(
|
|
144
|
+
None,
|
|
145
|
+
"--output",
|
|
146
|
+
"-o",
|
|
147
|
+
help="Output format. Overrides the global --output flag.",
|
|
148
|
+
case_sensitive=False,
|
|
149
|
+
),
|
|
150
|
+
) -> None:
|
|
151
|
+
"""List product groups with the count of products in each.
|
|
152
|
+
|
|
153
|
+
Wraps ``GET /products/groups``. Useful before
|
|
154
|
+
``impreza catalog products --group <name>`` to find the right
|
|
155
|
+
filter value.
|
|
156
|
+
"""
|
|
157
|
+
state = from_typer_context(typer_ctx)
|
|
158
|
+
fmt = resolve_output(state, output)
|
|
159
|
+
|
|
160
|
+
with make_client_or_exit(state) as client:
|
|
161
|
+
try:
|
|
162
|
+
groups = client.catalog.product_groups()
|
|
163
|
+
except ApiError as exc:
|
|
164
|
+
_exit_on_api_error(exc)
|
|
165
|
+
|
|
166
|
+
if not groups:
|
|
167
|
+
typer.echo("No product groups defined yet.")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
rows = [
|
|
171
|
+
{"id": g.id, "name": g.name, "product_count": g.product_count}
|
|
172
|
+
for g in groups
|
|
173
|
+
]
|
|
174
|
+
print_table(
|
|
175
|
+
f"Product groups ({len(rows)})",
|
|
176
|
+
rows,
|
|
177
|
+
columns=["id", "name", "product_count"],
|
|
178
|
+
fmt=fmt,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ── catalog tlds ─────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _year_1_price(prices: dict[str, float]) -> float | None:
|
|
186
|
+
"""Pull out the 1-year price from a ``{"1": x, "2": y}`` map.
|
|
187
|
+
Returns None when 1-year isn't offered (some registrars require
|
|
188
|
+
multi-year minimums)."""
|
|
189
|
+
raw = prices.get("1")
|
|
190
|
+
return float(raw) if raw is not None else None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.command("tlds")
|
|
194
|
+
def tlds(
|
|
195
|
+
typer_ctx: typer.Context,
|
|
196
|
+
filter_: str | None = typer.Option(
|
|
197
|
+
None,
|
|
198
|
+
"--filter",
|
|
199
|
+
"-f",
|
|
200
|
+
help=(
|
|
201
|
+
"Comma-separated list of TLDs (e.g. '.com,.net,.io'). "
|
|
202
|
+
"Without a filter, the full TLD catalog is returned."
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
output: OutputFormat | None = typer.Option(
|
|
206
|
+
None,
|
|
207
|
+
"--output",
|
|
208
|
+
"-o",
|
|
209
|
+
help="Output format. Overrides the global --output flag.",
|
|
210
|
+
case_sensitive=False,
|
|
211
|
+
),
|
|
212
|
+
) -> None:
|
|
213
|
+
"""List domain TLD pricing.
|
|
214
|
+
|
|
215
|
+
Wraps ``GET /domains/pricing``. Table mode shows tld / currency /
|
|
216
|
+
1-year register / 1-year renew / cheapest-overall. JSON / YAML
|
|
217
|
+
emit the full :class:`TldPricing` model with per-year pricing
|
|
218
|
+
dicts for both register and renew.
|
|
219
|
+
"""
|
|
220
|
+
state = from_typer_context(typer_ctx)
|
|
221
|
+
fmt = resolve_output(state, output)
|
|
222
|
+
|
|
223
|
+
with make_client_or_exit(state) as client:
|
|
224
|
+
try:
|
|
225
|
+
items: list[TldPricing] = client.catalog.tlds(filter=filter_)
|
|
226
|
+
except ApiError as exc:
|
|
227
|
+
_exit_on_api_error(exc)
|
|
228
|
+
|
|
229
|
+
if not items:
|
|
230
|
+
msg = (
|
|
231
|
+
f"No TLDs match the filter: {filter_!r}."
|
|
232
|
+
if filter_
|
|
233
|
+
else "No TLDs in the catalog yet."
|
|
234
|
+
)
|
|
235
|
+
typer.echo(msg)
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
if fmt is OutputFormat.TABLE:
|
|
239
|
+
rows: list[dict[str, Any]] = []
|
|
240
|
+
for t in items:
|
|
241
|
+
reg_1y = _year_1_price(t.register_prices)
|
|
242
|
+
ren_1y = _year_1_price(t.renew_prices)
|
|
243
|
+
rows.append(
|
|
244
|
+
{
|
|
245
|
+
"tld": t.tld,
|
|
246
|
+
"currency": t.currency,
|
|
247
|
+
"register_1y": f"{reg_1y:.2f}" if reg_1y is not None else "-",
|
|
248
|
+
"renew_1y": f"{ren_1y:.2f}" if ren_1y is not None else "-",
|
|
249
|
+
"cheapest": (
|
|
250
|
+
f"{t.cheapest:.2f}" if t.cheapest is not None else "-"
|
|
251
|
+
),
|
|
252
|
+
"min_years": t.min_years,
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
print_table(
|
|
256
|
+
f"TLDs ({len(rows)})",
|
|
257
|
+
rows,
|
|
258
|
+
columns=[
|
|
259
|
+
"tld",
|
|
260
|
+
"currency",
|
|
261
|
+
"register_1y",
|
|
262
|
+
"renew_1y",
|
|
263
|
+
"cheapest",
|
|
264
|
+
"min_years",
|
|
265
|
+
],
|
|
266
|
+
fmt=fmt,
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
rows = [t.model_dump(by_alias=True) for t in items]
|
|
270
|
+
print_table("TLDs", rows, fmt=fmt)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""``impreza context`` subcommand surface.
|
|
2
|
+
|
|
3
|
+
Five operations:
|
|
4
|
+
|
|
5
|
+
* ``impreza context create <name> --key ... --secret ... [--base-url ...]
|
|
6
|
+
[--default-output table|json|yaml] [--overwrite]``
|
|
7
|
+
* ``impreza context use <name>``
|
|
8
|
+
* ``impreza context list``
|
|
9
|
+
* ``impreza context current``
|
|
10
|
+
* ``impreza context delete <name> [--yes]``
|
|
11
|
+
|
|
12
|
+
The first context created becomes the default automatically. Names
|
|
13
|
+
must match ``[A-Za-z0-9_-]{1,50}`` (so they never need quoting in
|
|
14
|
+
shell commands).
|
|
15
|
+
|
|
16
|
+
The config file lives at ``$XDG_CONFIG_HOME/impreza/config.toml``
|
|
17
|
+
(Linux), ``~/Library/Application Support/impreza/config.toml`` (macOS),
|
|
18
|
+
or ``%APPDATA%\\impreza\\config.toml`` (Windows). Override with
|
|
19
|
+
``IMPREZA_CONFIG=/path/to/config.toml`` for tests or alternate
|
|
20
|
+
layouts.
|
|
21
|
+
|
|
22
|
+
Errors render as red ``Error:`` lines on stderr and exit non-zero so
|
|
23
|
+
scripts can detect failure cleanly. Stdout stays reserved for the
|
|
24
|
+
success output.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import typer
|
|
30
|
+
|
|
31
|
+
from ..config import (
|
|
32
|
+
Config,
|
|
33
|
+
ConfigError,
|
|
34
|
+
ContextAlreadyExists,
|
|
35
|
+
ContextNotFound,
|
|
36
|
+
InvalidContextName,
|
|
37
|
+
NoActiveContext,
|
|
38
|
+
NoContextsConfigured,
|
|
39
|
+
)
|
|
40
|
+
from ..output import OutputFormat, error, info, print_dict, print_table, success
|
|
41
|
+
from ..state import from_typer_context, resolve_output
|
|
42
|
+
|
|
43
|
+
app = typer.Typer(
|
|
44
|
+
name="context",
|
|
45
|
+
help="Manage local CLI contexts (named credential sets).",
|
|
46
|
+
no_args_is_help=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── helpers ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _exit_on_config_error(exc: ConfigError) -> None:
|
|
54
|
+
"""Print a friendly message and exit 1. Used by every command's
|
|
55
|
+
top-level error handler so error formatting stays consistent."""
|
|
56
|
+
error(str(exc))
|
|
57
|
+
raise typer.Exit(code=1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _mask_secret(value: str) -> str:
|
|
61
|
+
"""Render a secret string as ``imp_abc...xyz`` (first/last 4 chars
|
|
62
|
+
of the suffix; never the middle). Used in human-readable output
|
|
63
|
+
to confirm "yes the right key is loaded" without leaking it."""
|
|
64
|
+
if len(value) <= 12:
|
|
65
|
+
return "[dim]<short>[/]"
|
|
66
|
+
return f"{value[:8]}…{value[-4:]}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── commands ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command("create")
|
|
73
|
+
def create(
|
|
74
|
+
name: str = typer.Argument(..., help="Context name (alphanumeric, '-', '_')."),
|
|
75
|
+
key: str = typer.Option(..., "--key", "-k", help="API key (`imp_...`)."),
|
|
76
|
+
secret: str = typer.Option(
|
|
77
|
+
..., "--secret", "-s", help="API secret (64 hex chars).",
|
|
78
|
+
),
|
|
79
|
+
base_url: str | None = typer.Option(
|
|
80
|
+
None,
|
|
81
|
+
"--base-url",
|
|
82
|
+
help="Optional API base URL override (defaults to "
|
|
83
|
+
"https://api.imprezahost.com/v1).",
|
|
84
|
+
),
|
|
85
|
+
default_output: OutputFormat | None = typer.Option(
|
|
86
|
+
None,
|
|
87
|
+
"--default-output",
|
|
88
|
+
help="Optional default output format for this context.",
|
|
89
|
+
case_sensitive=False,
|
|
90
|
+
),
|
|
91
|
+
overwrite: bool = typer.Option(
|
|
92
|
+
False,
|
|
93
|
+
"--overwrite",
|
|
94
|
+
help="Replace an existing context with the same name.",
|
|
95
|
+
),
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Create a new context. The first one created is auto-set as
|
|
98
|
+
the default."""
|
|
99
|
+
cfg = Config.load()
|
|
100
|
+
try:
|
|
101
|
+
ctx = cfg.add_context(
|
|
102
|
+
name,
|
|
103
|
+
api_key=key,
|
|
104
|
+
api_secret=secret,
|
|
105
|
+
base_url=base_url,
|
|
106
|
+
default_output=default_output.value if default_output else None,
|
|
107
|
+
overwrite=overwrite,
|
|
108
|
+
)
|
|
109
|
+
except (ContextAlreadyExists, InvalidContextName) as exc:
|
|
110
|
+
_exit_on_config_error(exc)
|
|
111
|
+
cfg.save()
|
|
112
|
+
|
|
113
|
+
is_default = cfg.default_context == name
|
|
114
|
+
success(
|
|
115
|
+
f"Context {ctx.name!r} created"
|
|
116
|
+
+ (" and set as default." if is_default else ".")
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command("use")
|
|
121
|
+
def use(
|
|
122
|
+
name: str = typer.Argument(..., help="Context name to switch to."),
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Set the default context."""
|
|
125
|
+
cfg = Config.load()
|
|
126
|
+
try:
|
|
127
|
+
cfg.use_context(name)
|
|
128
|
+
except ContextNotFound as exc:
|
|
129
|
+
_exit_on_config_error(exc)
|
|
130
|
+
cfg.save()
|
|
131
|
+
success(f"Now using context {name!r}.")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command("list")
|
|
135
|
+
def list_contexts(
|
|
136
|
+
typer_ctx: typer.Context,
|
|
137
|
+
output: OutputFormat | None = typer.Option(
|
|
138
|
+
None,
|
|
139
|
+
"--output",
|
|
140
|
+
"-o",
|
|
141
|
+
help="Output format. Overrides the global --output flag for this command.",
|
|
142
|
+
case_sensitive=False,
|
|
143
|
+
),
|
|
144
|
+
) -> None:
|
|
145
|
+
"""List all configured contexts with their masked credentials."""
|
|
146
|
+
fmt = resolve_output(from_typer_context(typer_ctx), output)
|
|
147
|
+
cfg = Config.load()
|
|
148
|
+
rows = [
|
|
149
|
+
{
|
|
150
|
+
"name": ctx.name,
|
|
151
|
+
"default": cfg.default_context == ctx.name,
|
|
152
|
+
"api_key": _mask_secret(ctx.api_key)
|
|
153
|
+
if fmt is OutputFormat.TABLE
|
|
154
|
+
else ctx.api_key,
|
|
155
|
+
"base_url": ctx.base_url,
|
|
156
|
+
"default_output": ctx.default_output,
|
|
157
|
+
}
|
|
158
|
+
for ctx in (cfg.contexts[name] for name in cfg.list_contexts())
|
|
159
|
+
]
|
|
160
|
+
if not rows:
|
|
161
|
+
typer.echo("No contexts configured. Run `impreza context create <name>`.")
|
|
162
|
+
return
|
|
163
|
+
print_table(
|
|
164
|
+
"Contexts",
|
|
165
|
+
rows,
|
|
166
|
+
columns=["name", "default", "api_key", "base_url", "default_output"],
|
|
167
|
+
fmt=fmt,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command("current")
|
|
172
|
+
def current(
|
|
173
|
+
typer_ctx: typer.Context,
|
|
174
|
+
output: OutputFormat | None = typer.Option(
|
|
175
|
+
None,
|
|
176
|
+
"--output",
|
|
177
|
+
"-o",
|
|
178
|
+
help="Output format. Overrides the global --output flag for this command.",
|
|
179
|
+
case_sensitive=False,
|
|
180
|
+
),
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Show the active (default) context's metadata. Exits non-zero
|
|
183
|
+
when no contexts are configured or no default is set."""
|
|
184
|
+
fmt = resolve_output(from_typer_context(typer_ctx), output)
|
|
185
|
+
cfg = Config.load()
|
|
186
|
+
try:
|
|
187
|
+
ctx = cfg.get_context()
|
|
188
|
+
except (NoContextsConfigured, NoActiveContext, ContextNotFound) as exc:
|
|
189
|
+
_exit_on_config_error(exc)
|
|
190
|
+
|
|
191
|
+
print_dict(
|
|
192
|
+
"Current context",
|
|
193
|
+
{
|
|
194
|
+
"name": ctx.name,
|
|
195
|
+
"api_key": _mask_secret(ctx.api_key)
|
|
196
|
+
if fmt is OutputFormat.TABLE
|
|
197
|
+
else ctx.api_key,
|
|
198
|
+
"base_url": ctx.base_url or "<default>",
|
|
199
|
+
"default_output": ctx.default_output or "<inherit>",
|
|
200
|
+
},
|
|
201
|
+
fmt=fmt,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.command("delete")
|
|
206
|
+
def delete(
|
|
207
|
+
name: str = typer.Argument(..., help="Context name to delete."),
|
|
208
|
+
yes: bool = typer.Option(
|
|
209
|
+
False,
|
|
210
|
+
"--yes",
|
|
211
|
+
"-y",
|
|
212
|
+
help="Skip the interactive confirmation prompt.",
|
|
213
|
+
),
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Delete a context. Prompts for confirmation unless ``--yes``."""
|
|
216
|
+
cfg = Config.load()
|
|
217
|
+
if name not in cfg.contexts:
|
|
218
|
+
_exit_on_config_error(ContextNotFound(f"Context {name!r} does not exist."))
|
|
219
|
+
|
|
220
|
+
if not yes and not typer.confirm(f"Delete context {name!r}?", default=False):
|
|
221
|
+
typer.echo("Cancelled.")
|
|
222
|
+
raise typer.Exit(code=0)
|
|
223
|
+
|
|
224
|
+
was_default = cfg.default_context == name
|
|
225
|
+
cfg.remove_context(name)
|
|
226
|
+
cfg.save()
|
|
227
|
+
|
|
228
|
+
success(f"Context {name!r} deleted.")
|
|
229
|
+
if was_default and cfg.contexts:
|
|
230
|
+
info(
|
|
231
|
+
" (no default context now — pick one with `impreza context use <name>`.)"
|
|
232
|
+
)
|