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,198 @@
|
|
|
1
|
+
"""``impreza invoice`` subcommand surface — Phase 2.6.
|
|
2
|
+
|
|
3
|
+
Read-only invoice commands over `c.invoices.*`:
|
|
4
|
+
|
|
5
|
+
* ``impreza invoice list [--status STATUS] [-o ...]``
|
|
6
|
+
Wraps ``c.invoices.list(status=...)``. Multi-row table with
|
|
7
|
+
id / status / date / due_date / total / currency.
|
|
8
|
+
* ``impreza invoice show <id> [-o ...]``
|
|
9
|
+
Wraps ``c.invoices.get(id)``. Field/value detail with line
|
|
10
|
+
items as a sub-table (or JSON-array key in non-table modes).
|
|
11
|
+
|
|
12
|
+
Pay-from-balance (`POST /invoices/{id}/pay`) lands in Phase 3
|
|
13
|
+
alongside the rest of the mutating CLI surface.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from impreza.exceptions import ApiError, ResourceNotFound
|
|
22
|
+
|
|
23
|
+
from ..output import OutputFormat, error, print_dict, print_table
|
|
24
|
+
from ..sdk import make_client_or_exit
|
|
25
|
+
from ..state import from_typer_context, resolve_output
|
|
26
|
+
from ._helpers import exit_on_api_error as _exit_on_api_error
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(
|
|
29
|
+
name="invoice",
|
|
30
|
+
help="Read invoices on your account.",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_LIST_COLUMNS = ["id", "invoice_num", "status", "date", "due_date", "total"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ── invoice list ────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("list")
|
|
42
|
+
def list_invoices(
|
|
43
|
+
typer_ctx: typer.Context,
|
|
44
|
+
status: str | None = typer.Option(
|
|
45
|
+
None,
|
|
46
|
+
"--status",
|
|
47
|
+
help=(
|
|
48
|
+
"Filter by invoice status (Unpaid, Paid, Cancelled, "
|
|
49
|
+
"Refunded, Collections). Forwarded verbatim to the API; "
|
|
50
|
+
"case is preserved on the wire."
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
output: OutputFormat | None = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--output",
|
|
56
|
+
"-o",
|
|
57
|
+
help="Output format. Overrides the global --output flag.",
|
|
58
|
+
case_sensitive=False,
|
|
59
|
+
),
|
|
60
|
+
) -> None:
|
|
61
|
+
"""List invoices on the authenticated client's account.
|
|
62
|
+
|
|
63
|
+
Wraps ``GET /invoices``. The current API caps the response at
|
|
64
|
+
100 entries per call (no pagination metadata yet — tracked as
|
|
65
|
+
a 1.3 follow-up). When the account has more than 100 invoices,
|
|
66
|
+
only the most recent 100 come back; use your Impreza Account
|
|
67
|
+
for full history until pagination ships.
|
|
68
|
+
"""
|
|
69
|
+
state = from_typer_context(typer_ctx)
|
|
70
|
+
fmt = resolve_output(state, output)
|
|
71
|
+
|
|
72
|
+
with make_client_or_exit(state) as client:
|
|
73
|
+
try:
|
|
74
|
+
invoices = client.invoices.list(status=status)
|
|
75
|
+
except ApiError as exc:
|
|
76
|
+
_exit_on_api_error(exc)
|
|
77
|
+
|
|
78
|
+
if not invoices:
|
|
79
|
+
if status:
|
|
80
|
+
typer.echo(f"No invoices with status {status!r} on this account.")
|
|
81
|
+
else:
|
|
82
|
+
typer.echo("No invoices on this account yet.")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if fmt is OutputFormat.TABLE:
|
|
86
|
+
rows: list[dict[str, Any]] = [
|
|
87
|
+
{
|
|
88
|
+
"id": inv.id,
|
|
89
|
+
"invoice_num": inv.invoice_num,
|
|
90
|
+
"status": inv.status,
|
|
91
|
+
"date": inv.date,
|
|
92
|
+
"due_date": inv.due_date,
|
|
93
|
+
"total": f"{inv.total:.2f}",
|
|
94
|
+
}
|
|
95
|
+
for inv in invoices
|
|
96
|
+
]
|
|
97
|
+
title = (
|
|
98
|
+
f"Invoices ({len(rows)} {status!r})"
|
|
99
|
+
if status
|
|
100
|
+
else f"Invoices ({len(rows)} total)"
|
|
101
|
+
)
|
|
102
|
+
print_table(title, rows, columns=_LIST_COLUMNS, fmt=fmt)
|
|
103
|
+
else:
|
|
104
|
+
rows = [inv.model_dump() for inv in invoices]
|
|
105
|
+
print_table("Invoices", rows, fmt=fmt)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── invoice show ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("show")
|
|
112
|
+
def show(
|
|
113
|
+
typer_ctx: typer.Context,
|
|
114
|
+
invoice_id: int = typer.Argument(..., help="Invoice id to inspect."),
|
|
115
|
+
output: OutputFormat | None = typer.Option(
|
|
116
|
+
None,
|
|
117
|
+
"--output",
|
|
118
|
+
"-o",
|
|
119
|
+
help="Output format. Overrides the global --output flag.",
|
|
120
|
+
case_sensitive=False,
|
|
121
|
+
),
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Show full invoice detail with line items and transactions.
|
|
124
|
+
|
|
125
|
+
Wraps ``GET /invoices/{id}``. Table mode renders the invoice
|
|
126
|
+
summary as a Field/Value table and follows it with sub-tables
|
|
127
|
+
for line items and recorded transactions. JSON / YAML emit
|
|
128
|
+
the full :class:`InvoiceDetail` model with items / transactions
|
|
129
|
+
as nested arrays.
|
|
130
|
+
"""
|
|
131
|
+
state = from_typer_context(typer_ctx)
|
|
132
|
+
fmt = resolve_output(state, output)
|
|
133
|
+
|
|
134
|
+
with make_client_or_exit(state) as client:
|
|
135
|
+
try:
|
|
136
|
+
inv = client.invoices.get(invoice_id)
|
|
137
|
+
except ResourceNotFound:
|
|
138
|
+
error(f"Invoice {invoice_id} not found on this account.")
|
|
139
|
+
raise typer.Exit(code=1) from None
|
|
140
|
+
except ApiError as exc:
|
|
141
|
+
_exit_on_api_error(exc)
|
|
142
|
+
|
|
143
|
+
if fmt is not OutputFormat.TABLE:
|
|
144
|
+
# Single JSON / YAML object with items + transactions nested.
|
|
145
|
+
print_dict(f"Invoice {invoice_id}", inv.model_dump(), fmt=fmt)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
# Table mode: header dict + sub-tables for items & transactions.
|
|
149
|
+
header: dict[str, Any] = {
|
|
150
|
+
"id": inv.id,
|
|
151
|
+
"invoice_num": inv.invoice_num,
|
|
152
|
+
"status": inv.status,
|
|
153
|
+
"date": inv.date,
|
|
154
|
+
"due_date": inv.due_date,
|
|
155
|
+
"date_paid": inv.date_paid,
|
|
156
|
+
"subtotal": f"{inv.subtotal:.2f}",
|
|
157
|
+
"credit": f"{inv.credit:.2f}",
|
|
158
|
+
"tax": f"{inv.tax:.2f}",
|
|
159
|
+
"total": f"{inv.total:.2f}",
|
|
160
|
+
"payment_method": inv.payment_method,
|
|
161
|
+
}
|
|
162
|
+
print_dict(f"Invoice {invoice_id}", header, fmt=fmt)
|
|
163
|
+
|
|
164
|
+
if inv.items:
|
|
165
|
+
item_rows = [
|
|
166
|
+
{
|
|
167
|
+
"id": it.id,
|
|
168
|
+
"type": it.type,
|
|
169
|
+
"description": it.description,
|
|
170
|
+
"amount": f"{it.amount:.2f}",
|
|
171
|
+
"taxed": it.taxed,
|
|
172
|
+
}
|
|
173
|
+
for it in inv.items
|
|
174
|
+
]
|
|
175
|
+
print_table(
|
|
176
|
+
f"Line items ({len(item_rows)})",
|
|
177
|
+
item_rows,
|
|
178
|
+
columns=["id", "type", "description", "amount", "taxed"],
|
|
179
|
+
fmt=fmt,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if inv.transactions:
|
|
183
|
+
tx_rows = [
|
|
184
|
+
{
|
|
185
|
+
"id": t.id,
|
|
186
|
+
"date": t.date,
|
|
187
|
+
"gateway": t.gateway,
|
|
188
|
+
"amount": f"{t.amount:.2f}" if t.amount is not None else "-",
|
|
189
|
+
"transaction_id": t.transaction_id,
|
|
190
|
+
}
|
|
191
|
+
for t in inv.transactions
|
|
192
|
+
]
|
|
193
|
+
print_table(
|
|
194
|
+
f"Transactions ({len(tx_rows)})",
|
|
195
|
+
tx_rows,
|
|
196
|
+
columns=["id", "date", "gateway", "amount", "transaction_id"],
|
|
197
|
+
fmt=fmt,
|
|
198
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""``impreza key`` subcommand surface — Phase 2.6.
|
|
2
|
+
|
|
3
|
+
Currently one verb:
|
|
4
|
+
|
|
5
|
+
* ``impreza key whoami [-o ...]``
|
|
6
|
+
Wraps ``c.account.api_key_self()`` (which itself wraps
|
|
7
|
+
``GET /v1/account/api-keys/self``). Prints the active API
|
|
8
|
+
key's identity — public prefix, label, status, last-used
|
|
9
|
+
timestamp, rate limit, and the IP whitelist as a sub-table.
|
|
10
|
+
The full secret is never returned by the server, so no
|
|
11
|
+
masking is needed here.
|
|
12
|
+
|
|
13
|
+
Key management verbs (`create` / `revoke` / `list`) hit the Impreza Account
|
|
14
|
+
client area portal API rather than this addon's surface, so they
|
|
15
|
+
live in a separate fase that wires the portal API as an SDK
|
|
16
|
+
resource.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import typer
|
|
24
|
+
from impreza.exceptions import ApiError
|
|
25
|
+
|
|
26
|
+
from ..output import OutputFormat, print_dict, print_table
|
|
27
|
+
from ..sdk import make_client_or_exit
|
|
28
|
+
from ..state import from_typer_context, resolve_output
|
|
29
|
+
from ._helpers import exit_on_api_error as _exit_on_api_error
|
|
30
|
+
|
|
31
|
+
app = typer.Typer(
|
|
32
|
+
name="key",
|
|
33
|
+
help="Inspect the active API key's identity and IP whitelist.",
|
|
34
|
+
no_args_is_help=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("whoami")
|
|
39
|
+
def whoami(
|
|
40
|
+
typer_ctx: typer.Context,
|
|
41
|
+
output: OutputFormat | None = typer.Option(
|
|
42
|
+
None,
|
|
43
|
+
"--output",
|
|
44
|
+
"-o",
|
|
45
|
+
help="Output format. Overrides the global --output flag.",
|
|
46
|
+
case_sensitive=False,
|
|
47
|
+
),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Identity of the API key making this call.
|
|
50
|
+
|
|
51
|
+
Useful for confirming which context's credentials are loaded
|
|
52
|
+
and which IP the server is observing — common debugging
|
|
53
|
+
scenario when a request is rejected with `IP_NOT_WHITELISTED`.
|
|
54
|
+
"""
|
|
55
|
+
state = from_typer_context(typer_ctx)
|
|
56
|
+
fmt = resolve_output(state, output)
|
|
57
|
+
|
|
58
|
+
with make_client_or_exit(state) as client:
|
|
59
|
+
try:
|
|
60
|
+
ident = client.account.api_key_self()
|
|
61
|
+
except ApiError as exc:
|
|
62
|
+
_exit_on_api_error(exc)
|
|
63
|
+
|
|
64
|
+
if fmt is not OutputFormat.TABLE:
|
|
65
|
+
print_dict("API key identity", ident.model_dump(), fmt=fmt)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Table mode: Field/Value header + sub-table for the whitelist.
|
|
69
|
+
header: dict[str, Any] = {
|
|
70
|
+
"id": ident.id,
|
|
71
|
+
"client_id": ident.client_id,
|
|
72
|
+
"prefix": ident.prefix,
|
|
73
|
+
"label": ident.label,
|
|
74
|
+
"status": ident.status,
|
|
75
|
+
"last_used_at": ident.last_used_at,
|
|
76
|
+
"created_at": ident.created_at,
|
|
77
|
+
"rate_limit_per_minute": ident.rate_limit_per_minute,
|
|
78
|
+
"request_ip": ident.request_ip,
|
|
79
|
+
}
|
|
80
|
+
print_dict("API key identity", header, fmt=fmt)
|
|
81
|
+
|
|
82
|
+
if ident.ip_whitelist:
|
|
83
|
+
rows = [
|
|
84
|
+
{
|
|
85
|
+
"id": ip.id,
|
|
86
|
+
"ip_address": ip.ip_address,
|
|
87
|
+
"label": ip.label,
|
|
88
|
+
"current": ip.ip_address == ident.request_ip,
|
|
89
|
+
"created_at": ip.created_at,
|
|
90
|
+
}
|
|
91
|
+
for ip in ident.ip_whitelist
|
|
92
|
+
]
|
|
93
|
+
print_table(
|
|
94
|
+
f"IP whitelist ({len(rows)})",
|
|
95
|
+
rows,
|
|
96
|
+
columns=["id", "ip_address", "label", "current", "created_at"],
|
|
97
|
+
fmt=fmt,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
# No whitelist entries means "this key has no IP restrictions"
|
|
101
|
+
# — surface that clearly. Server enforces auth via the key
|
|
102
|
+
# secret regardless, but no whitelist is unusual on a real
|
|
103
|
+
# account.
|
|
104
|
+
typer.echo("(no IP whitelist configured on this key)")
|