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.
@@ -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)")