impreza-sdk 0.3.0__tar.gz

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.
Files changed (75) hide show
  1. impreza_sdk-0.3.0/PKG-INFO +311 -0
  2. impreza_sdk-0.3.0/README.md +275 -0
  3. impreza_sdk-0.3.0/impreza/__init__.py +171 -0
  4. impreza_sdk-0.3.0/impreza/_http.py +277 -0
  5. impreza_sdk-0.3.0/impreza/_http_async.py +270 -0
  6. impreza_sdk-0.3.0/impreza/_pagination.py +72 -0
  7. impreza_sdk-0.3.0/impreza/_polling.py +280 -0
  8. impreza_sdk-0.3.0/impreza/_topup.py +311 -0
  9. impreza_sdk-0.3.0/impreza/_tor.py +81 -0
  10. impreza_sdk-0.3.0/impreza/async_client.py +125 -0
  11. impreza_sdk-0.3.0/impreza/client.py +145 -0
  12. impreza_sdk-0.3.0/impreza/exceptions.py +235 -0
  13. impreza_sdk-0.3.0/impreza/models/__init__.py +85 -0
  14. impreza_sdk-0.3.0/impreza/models/account.py +97 -0
  15. impreza_sdk-0.3.0/impreza/models/dns.py +23 -0
  16. impreza_sdk-0.3.0/impreza/models/domain.py +68 -0
  17. impreza_sdk-0.3.0/impreza/models/email.py +24 -0
  18. impreza_sdk-0.3.0/impreza/models/invoice.py +60 -0
  19. impreza_sdk-0.3.0/impreza/models/order.py +69 -0
  20. impreza_sdk-0.3.0/impreza/models/product.py +116 -0
  21. impreza_sdk-0.3.0/impreza/models/service.py +36 -0
  22. impreza_sdk-0.3.0/impreza/models/tld.py +30 -0
  23. impreza_sdk-0.3.0/impreza/models/vps.py +30 -0
  24. impreza_sdk-0.3.0/impreza/models/vps_extras.py +140 -0
  25. impreza_sdk-0.3.0/impreza/models/webhook.py +104 -0
  26. impreza_sdk-0.3.0/impreza/py.typed +0 -0
  27. impreza_sdk-0.3.0/impreza/resources/__init__.py +96 -0
  28. impreza_sdk-0.3.0/impreza/resources/account.py +171 -0
  29. impreza_sdk-0.3.0/impreza/resources/catalog.py +139 -0
  30. impreza_sdk-0.3.0/impreza/resources/domains.py +454 -0
  31. impreza_sdk-0.3.0/impreza/resources/email.py +228 -0
  32. impreza_sdk-0.3.0/impreza/resources/hosting.py +92 -0
  33. impreza_sdk-0.3.0/impreza/resources/invoices.py +69 -0
  34. impreza_sdk-0.3.0/impreza/resources/orders.py +462 -0
  35. impreza_sdk-0.3.0/impreza/resources/services.py +136 -0
  36. impreza_sdk-0.3.0/impreza/resources/vps.py +924 -0
  37. impreza_sdk-0.3.0/impreza/resources/vps_cloud.py +313 -0
  38. impreza_sdk-0.3.0/impreza/resources/vps_proxmox.py +350 -0
  39. impreza_sdk-0.3.0/impreza/resources/webhooks.py +284 -0
  40. impreza_sdk-0.3.0/impreza/webhooks.py +143 -0
  41. impreza_sdk-0.3.0/impreza_sdk.egg-info/PKG-INFO +311 -0
  42. impreza_sdk-0.3.0/impreza_sdk.egg-info/SOURCES.txt +73 -0
  43. impreza_sdk-0.3.0/impreza_sdk.egg-info/dependency_links.txt +1 -0
  44. impreza_sdk-0.3.0/impreza_sdk.egg-info/requires.txt +13 -0
  45. impreza_sdk-0.3.0/impreza_sdk.egg-info/top_level.txt +1 -0
  46. impreza_sdk-0.3.0/pyproject.toml +80 -0
  47. impreza_sdk-0.3.0/setup.cfg +4 -0
  48. impreza_sdk-0.3.0/tests/conftest.py +31 -0
  49. impreza_sdk-0.3.0/tests/test_account_smoke.py +38 -0
  50. impreza_sdk-0.3.0/tests/test_account_topup_unit.py +439 -0
  51. impreza_sdk-0.3.0/tests/test_account_unit.py +252 -0
  52. impreza_sdk-0.3.0/tests/test_async_account_smoke.py +43 -0
  53. impreza_sdk-0.3.0/tests/test_async_account_unit.py +148 -0
  54. impreza_sdk-0.3.0/tests/test_catalog_unit.py +252 -0
  55. impreza_sdk-0.3.0/tests/test_domains_unit.py +529 -0
  56. impreza_sdk-0.3.0/tests/test_email_unit.py +240 -0
  57. impreza_sdk-0.3.0/tests/test_hosting_unit.py +134 -0
  58. impreza_sdk-0.3.0/tests/test_invoices_unit.py +169 -0
  59. impreza_sdk-0.3.0/tests/test_orders_unit.py +418 -0
  60. impreza_sdk-0.3.0/tests/test_pagination.py +61 -0
  61. impreza_sdk-0.3.0/tests/test_phase_1_3_smoke.py +73 -0
  62. impreza_sdk-0.3.0/tests/test_phase_1_4a_smoke.py +50 -0
  63. impreza_sdk-0.3.0/tests/test_phase_1_4b_ii_smoke.py +90 -0
  64. impreza_sdk-0.3.0/tests/test_phase_1_4b_smoke.py +64 -0
  65. impreza_sdk-0.3.0/tests/test_phase_1_4c_smoke.py +122 -0
  66. impreza_sdk-0.3.0/tests/test_phase_1_4d_smoke.py +63 -0
  67. impreza_sdk-0.3.0/tests/test_phase_1_5_smoke.py +99 -0
  68. impreza_sdk-0.3.0/tests/test_phase_1_6_smoke.py +100 -0
  69. impreza_sdk-0.3.0/tests/test_phase_1_7_smoke.py +132 -0
  70. impreza_sdk-0.3.0/tests/test_polling_unit.py +410 -0
  71. impreza_sdk-0.3.0/tests/test_services_unit.py +250 -0
  72. impreza_sdk-0.3.0/tests/test_tor.py +69 -0
  73. impreza_sdk-0.3.0/tests/test_vps_specific_unit.py +716 -0
  74. impreza_sdk-0.3.0/tests/test_vps_unit.py +588 -0
  75. impreza_sdk-0.3.0/tests/test_webhooks_unit.py +484 -0
@@ -0,0 +1,311 @@
1
+ Metadata-Version: 2.4
2
+ Name: impreza-sdk
3
+ Version: 0.3.0
4
+ Summary: Official Python SDK 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: Issues, https://github.com/imprezahost/impreza-devkit/issues
11
+ Project-URL: Changelog, https://github.com/imprezahost/impreza-devkit/blob/master/CHANGELOG.md
12
+ Keywords: impreza,hosting,domains,vps,api,offshore,crypto
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: httpx[socks]>=0.27
26
+ Requires-Dist: pydantic>=2.5
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest>=8.0; extra == "test"
29
+ Requires-Dist: pytest-cov>=4.1; extra == "test"
30
+ Requires-Dist: respx>=0.20; extra == "test"
31
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
32
+ Provides-Extra: dev
33
+ Requires-Dist: ruff>=0.5; extra == "dev"
34
+ Requires-Dist: mypy>=1.8; extra == "dev"
35
+ Requires-Dist: pre-commit>=3.6; extra == "dev"
36
+
37
+ # `impreza-sdk` — Official Python SDK for Impreza Host
38
+
39
+ Type-safe, sync + async client for the Impreza Host public REST API
40
+ ([api.imprezahost.com](https://api.imprezahost.com)). Covers domains,
41
+ hosting (cPanel), managed email (Titan / Workspace), VPS (Proxmox +
42
+ Cloud) with smart dispatch, orders, invoices, webhooks, and
43
+ crypto top-up — all behind one client object.
44
+
45
+ ```bash
46
+ pip install impreza-sdk
47
+ ```
48
+
49
+ Requires Python 3.10+. See [`CHANGELOG.md`](../CHANGELOG.md) for the
50
+ release history.
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ from impreza import Client
56
+
57
+ # Reads IMPREZA_API_KEY and IMPREZA_API_SECRET from the environment.
58
+ # Passing them explicitly works too: Client(api_key="...", api_secret="...").
59
+ with Client.from_env() as c:
60
+ me = c.account.get()
61
+ print(me.balance, me.currency)
62
+
63
+ # List active services across all backends
64
+ for svc in c.account.services.list(status="Active"):
65
+ print(svc.id, svc.product, svc.status, svc.vps_backend)
66
+ ```
67
+
68
+ ## Authentication
69
+
70
+ Two headers, no signing:
71
+
72
+ - `IMPREZA_API_KEY` — `imp_` + 40 hex chars (public identifier)
73
+ - `IMPREZA_API_SECRET` — 64 hex chars (shown once at creation)
74
+
75
+ Generate keys in your Impreza Account. Add the source IP of every
76
+ machine that calls the API to the key's IP whitelist.
77
+
78
+ ```bash
79
+ export IMPREZA_API_KEY="imp_..."
80
+ export IMPREZA_API_SECRET="..."
81
+ ```
82
+
83
+ ## Crypto top-up
84
+
85
+ Top up account balance with BTC / XMR / TRX / USDT-TRC20 via the
86
+ existing `btcpayinline` gateway:
87
+
88
+ ```python
89
+ invoice = c.account.topup(amount=50, method="xmr")
90
+ print(invoice.payment_url) # btcpayinline URL
91
+ print(invoice.expires_at) # ISO timestamp, 2h from creation
92
+
93
+ # Poll until paid (or fail on cancellation / refund / expiry)
94
+ invoice.wait_until_paid(timeout=7200, poll_interval=30)
95
+ print(invoice.balance_after)
96
+ ```
97
+
98
+ For push delivery instead of polling, subscribe a webhook to the
99
+ `topup.paid` event (see below).
100
+
101
+ ## Domains
102
+
103
+ ```python
104
+ # Availability
105
+ print(c.domains.check(["example.com", "example.net"]))
106
+
107
+ # Lookup
108
+ domain = c.domains.get("example.com")
109
+ print(domain.status, domain.expiry_date)
110
+
111
+ # DNS CRUD (nested under domains)
112
+ records = c.domains.dns.list("example.com")
113
+ c.domains.dns.add("example.com", type="A", name="@", value="1.2.3.4", ttl=3600)
114
+ c.domains.dns.update("example.com", type="A", name="@",
115
+ old_value="1.2.3.4", new_value="5.6.7.8")
116
+ ```
117
+
118
+ ## VPS (smart dispatch)
119
+
120
+ One `c.vps.get(service_id)` call returns a backend-aware bound model.
121
+ The SDK looks up which backend (Proxmox or Cloud) the service is on
122
+ and routes operations transparently:
123
+
124
+ ```python
125
+ vps = c.vps.get(17988) # service id from c.account.services.list()
126
+ print(vps.backend) # "proxmox" or "cloud"
127
+
128
+ # Common surface — same on both backends
129
+ status = vps.status()
130
+ print(status.power_state) # "running" (Proxmox) / "online" (Cloud)
131
+ vps.reboot()
132
+
133
+ # Backend-specific sub-resources (Proxmox)
134
+ vps.snapshots.list()
135
+ op = vps.snapshots.rollback("pre-update") # returns Operation future
136
+ op.wait(timeout=600) # blocks until queue completes
137
+
138
+ # Backend-specific sub-resources (Cloud)
139
+ vps.images.list() # account-scoped
140
+ vps.rescue.enable()
141
+ ```
142
+
143
+ Wrong-backend access raises `BackendNotSupported` client-side — no
144
+ network call:
145
+
146
+ ```python
147
+ vps = c.vps.get(17987) # Cloud VPS
148
+ vps.snapshots.list()
149
+ # raises BackendNotSupported("snapshots is not supported on the 'cloud'
150
+ # VPS backend. Use vps.images instead.")
151
+ ```
152
+
153
+ ## Webhooks
154
+
155
+ Subscribe to events server-side:
156
+
157
+ ```python
158
+ sub = c.webhooks.create(
159
+ url="https://example.com/hooks/impreza",
160
+ events=["topup.paid", "vps.power_state_changed", "domain.*"],
161
+ )
162
+ print(sub.secret) # 64 hex chars; shown ONCE — store it
163
+ ```
164
+
165
+ Verify deliveries on your receiver (timing-safe HMAC-SHA256):
166
+
167
+ ```python
168
+ from impreza.webhooks import verify_signature, WebhookSignatureMismatch
169
+
170
+ # Inside your Flask / FastAPI / Django handler
171
+ try:
172
+ event = verify_signature(
173
+ body=request.body, # raw bytes, NOT a re-serialized JSON
174
+ signature_header=request.headers["X-Impreza-Signature"],
175
+ secret=os.environ["IMPREZA_WEBHOOK_SECRET"],
176
+ )
177
+ except WebhookSignatureMismatch:
178
+ return ("invalid signature", 401)
179
+
180
+ if event.type == "topup.paid":
181
+ credit_user(event.data.client_id, event.data.amount)
182
+ ```
183
+
184
+ ## Async
185
+
186
+ Every resource has an async counterpart via `AsyncClient`. **Reach for it
187
+ when you need high-fanout calls** — provisioning many VPSs, polling a
188
+ bulk of invoices in parallel, etc. For one-shot scripts and interactive
189
+ use the sync `Client` is simpler and has the same surface.
190
+
191
+ ```python
192
+ import asyncio
193
+ from impreza import AsyncClient
194
+
195
+ async def main():
196
+ async with AsyncClient.from_env() as c:
197
+ # Read calls fan out trivially:
198
+ me = await c.account.get()
199
+
200
+ # Concurrent reboot across every VPS — gather() returns when
201
+ # all complete (or any one raises). 10 calls take ~one
202
+ # round-trip's worth of wall-clock, not 10x.
203
+ vpss = await c.vps.list()
204
+ await asyncio.gather(*[v.reboot() for v in vpss])
205
+
206
+ # Async futures work the same way as their sync counterparts:
207
+ op = await c.vps.get(17988).snapshots.rollback("pre-update")
208
+ await op.wait(timeout=600) # AsyncOperation.wait()
209
+
210
+ invoice = await c.account.topup(amount=50, method="xmr")
211
+ await invoice.wait_until_paid(timeout=7200) # AsyncTopupInvoice
212
+
213
+ asyncio.run(main())
214
+ ```
215
+
216
+ The `impreza` CLI is **sync by design** — CLI invocations are one-shot
217
+ processes where the async-fanout speedup doesn't apply. The async path
218
+ exists for library users; everything the CLI does has a `Client.*`
219
+ equivalent. If you're integrating into a long-running async app
220
+ (FastAPI, an event consumer, a batch worker), import the SDK directly
221
+ and skip the CLI.
222
+
223
+ ## Tor routing
224
+
225
+ ```python
226
+ # Explicit
227
+ c = Client(api_key="...", api_secret="...",
228
+ proxy="socks5://127.0.0.1:9050")
229
+
230
+ # Opt-in via flag or env var
231
+ c = Client.from_env(use_tor=True) # equivalent to IMPREZA_USE_TOR=1
232
+
233
+ # Probe Tor; fall back to clearnet if not running
234
+ c = Client.from_env(auto_tor=True)
235
+ ```
236
+
237
+ Backed by `httpx[socks]`. Sync and async parity. The probe used by
238
+ `auto_tor=True` never raises — failure means clearnet.
239
+
240
+ ## Error handling
241
+
242
+ All errors subclass `ImprezaError`, so a single `except` catches
243
+ everything the SDK can throw:
244
+
245
+ ```python
246
+ from impreza import (
247
+ Client, ImprezaError,
248
+ InsufficientCredit, RateLimitExceeded, IpNotWhitelisted,
249
+ OperationFailed, TopupFailed,
250
+ )
251
+
252
+ try:
253
+ c.orders.create(product_id=12, billing_cycle="monthly", domain="example.com")
254
+ except InsufficientCredit as e:
255
+ needed = e.details.get("amount_needed")
256
+ print(f"Need ${needed} more — opening a top-up invoice")
257
+ inv = c.account.topup(amount=needed, method="btc")
258
+ print(inv.payment_url)
259
+ except RateLimitExceeded as e:
260
+ print(f"Rate limited — retry after {e.retry_after}s")
261
+ except IpNotWhitelisted:
262
+ print("Your current IP isn't on the API key's whitelist")
263
+ ```
264
+
265
+ ## Action polling
266
+
267
+ Long-running Proxmox operations return `Operation` futures:
268
+
269
+ ```python
270
+ op = c.vps.get(17988).backups.create()
271
+ print(op.uuid, op.status) # 'queued' initially
272
+ op.wait(timeout=1800, poll_interval=10)
273
+ print(op.is_success(), op.finished_at)
274
+ ```
275
+
276
+ `wait()` raises `OperationTimeout` if it doesn't finish, or
277
+ `OperationFailed` on a terminal failure state.
278
+
279
+ ## Development
280
+
281
+ ```bash
282
+ git clone https://github.com/imprezahost/impreza-devkit.git
283
+ cd impreza-devkit/sdk-python
284
+ python -m venv .venv
285
+ # Linux/macOS: source .venv/bin/activate
286
+ # Windows PowerShell: .venv\Scripts\Activate.ps1
287
+ pip install -e ".[test,dev]"
288
+
289
+ pytest # 266 unit tests + mocks
290
+ ruff check
291
+ mypy --strict impreza
292
+ ```
293
+
294
+ To run the live integration smokes:
295
+
296
+ ```bash
297
+ export IMPREZA_API_KEY="imp_..."
298
+ export IMPREZA_API_SECRET="..."
299
+ # Optional — only needed for the full topup decode smoke
300
+ export IMPREZA_TOPUP_INVOICE_ID="<id of an existing AddFunds invoice>"
301
+
302
+ pytest -v -s tests/
303
+ ```
304
+
305
+ The destructive tests (real top-up creation, real Proxmox backup
306
+ creation) are gated behind `IMPREZA_DESTRUCTIVE_TESTS=1` and skipped
307
+ by default.
308
+
309
+ ## License
310
+
311
+ MIT. See [`../LICENSE`](../LICENSE) at the repository root.
@@ -0,0 +1,275 @@
1
+ # `impreza-sdk` — Official Python SDK for Impreza Host
2
+
3
+ Type-safe, sync + async client for the Impreza Host public REST API
4
+ ([api.imprezahost.com](https://api.imprezahost.com)). Covers domains,
5
+ hosting (cPanel), managed email (Titan / Workspace), VPS (Proxmox +
6
+ Cloud) with smart dispatch, orders, invoices, webhooks, and
7
+ crypto top-up — all behind one client object.
8
+
9
+ ```bash
10
+ pip install impreza-sdk
11
+ ```
12
+
13
+ Requires Python 3.10+. See [`CHANGELOG.md`](../CHANGELOG.md) for the
14
+ release history.
15
+
16
+ ## Quickstart
17
+
18
+ ```python
19
+ from impreza import Client
20
+
21
+ # Reads IMPREZA_API_KEY and IMPREZA_API_SECRET from the environment.
22
+ # Passing them explicitly works too: Client(api_key="...", api_secret="...").
23
+ with Client.from_env() as c:
24
+ me = c.account.get()
25
+ print(me.balance, me.currency)
26
+
27
+ # List active services across all backends
28
+ for svc in c.account.services.list(status="Active"):
29
+ print(svc.id, svc.product, svc.status, svc.vps_backend)
30
+ ```
31
+
32
+ ## Authentication
33
+
34
+ Two headers, no signing:
35
+
36
+ - `IMPREZA_API_KEY` — `imp_` + 40 hex chars (public identifier)
37
+ - `IMPREZA_API_SECRET` — 64 hex chars (shown once at creation)
38
+
39
+ Generate keys in your Impreza Account. Add the source IP of every
40
+ machine that calls the API to the key's IP whitelist.
41
+
42
+ ```bash
43
+ export IMPREZA_API_KEY="imp_..."
44
+ export IMPREZA_API_SECRET="..."
45
+ ```
46
+
47
+ ## Crypto top-up
48
+
49
+ Top up account balance with BTC / XMR / TRX / USDT-TRC20 via the
50
+ existing `btcpayinline` gateway:
51
+
52
+ ```python
53
+ invoice = c.account.topup(amount=50, method="xmr")
54
+ print(invoice.payment_url) # btcpayinline URL
55
+ print(invoice.expires_at) # ISO timestamp, 2h from creation
56
+
57
+ # Poll until paid (or fail on cancellation / refund / expiry)
58
+ invoice.wait_until_paid(timeout=7200, poll_interval=30)
59
+ print(invoice.balance_after)
60
+ ```
61
+
62
+ For push delivery instead of polling, subscribe a webhook to the
63
+ `topup.paid` event (see below).
64
+
65
+ ## Domains
66
+
67
+ ```python
68
+ # Availability
69
+ print(c.domains.check(["example.com", "example.net"]))
70
+
71
+ # Lookup
72
+ domain = c.domains.get("example.com")
73
+ print(domain.status, domain.expiry_date)
74
+
75
+ # DNS CRUD (nested under domains)
76
+ records = c.domains.dns.list("example.com")
77
+ c.domains.dns.add("example.com", type="A", name="@", value="1.2.3.4", ttl=3600)
78
+ c.domains.dns.update("example.com", type="A", name="@",
79
+ old_value="1.2.3.4", new_value="5.6.7.8")
80
+ ```
81
+
82
+ ## VPS (smart dispatch)
83
+
84
+ One `c.vps.get(service_id)` call returns a backend-aware bound model.
85
+ The SDK looks up which backend (Proxmox or Cloud) the service is on
86
+ and routes operations transparently:
87
+
88
+ ```python
89
+ vps = c.vps.get(17988) # service id from c.account.services.list()
90
+ print(vps.backend) # "proxmox" or "cloud"
91
+
92
+ # Common surface — same on both backends
93
+ status = vps.status()
94
+ print(status.power_state) # "running" (Proxmox) / "online" (Cloud)
95
+ vps.reboot()
96
+
97
+ # Backend-specific sub-resources (Proxmox)
98
+ vps.snapshots.list()
99
+ op = vps.snapshots.rollback("pre-update") # returns Operation future
100
+ op.wait(timeout=600) # blocks until queue completes
101
+
102
+ # Backend-specific sub-resources (Cloud)
103
+ vps.images.list() # account-scoped
104
+ vps.rescue.enable()
105
+ ```
106
+
107
+ Wrong-backend access raises `BackendNotSupported` client-side — no
108
+ network call:
109
+
110
+ ```python
111
+ vps = c.vps.get(17987) # Cloud VPS
112
+ vps.snapshots.list()
113
+ # raises BackendNotSupported("snapshots is not supported on the 'cloud'
114
+ # VPS backend. Use vps.images instead.")
115
+ ```
116
+
117
+ ## Webhooks
118
+
119
+ Subscribe to events server-side:
120
+
121
+ ```python
122
+ sub = c.webhooks.create(
123
+ url="https://example.com/hooks/impreza",
124
+ events=["topup.paid", "vps.power_state_changed", "domain.*"],
125
+ )
126
+ print(sub.secret) # 64 hex chars; shown ONCE — store it
127
+ ```
128
+
129
+ Verify deliveries on your receiver (timing-safe HMAC-SHA256):
130
+
131
+ ```python
132
+ from impreza.webhooks import verify_signature, WebhookSignatureMismatch
133
+
134
+ # Inside your Flask / FastAPI / Django handler
135
+ try:
136
+ event = verify_signature(
137
+ body=request.body, # raw bytes, NOT a re-serialized JSON
138
+ signature_header=request.headers["X-Impreza-Signature"],
139
+ secret=os.environ["IMPREZA_WEBHOOK_SECRET"],
140
+ )
141
+ except WebhookSignatureMismatch:
142
+ return ("invalid signature", 401)
143
+
144
+ if event.type == "topup.paid":
145
+ credit_user(event.data.client_id, event.data.amount)
146
+ ```
147
+
148
+ ## Async
149
+
150
+ Every resource has an async counterpart via `AsyncClient`. **Reach for it
151
+ when you need high-fanout calls** — provisioning many VPSs, polling a
152
+ bulk of invoices in parallel, etc. For one-shot scripts and interactive
153
+ use the sync `Client` is simpler and has the same surface.
154
+
155
+ ```python
156
+ import asyncio
157
+ from impreza import AsyncClient
158
+
159
+ async def main():
160
+ async with AsyncClient.from_env() as c:
161
+ # Read calls fan out trivially:
162
+ me = await c.account.get()
163
+
164
+ # Concurrent reboot across every VPS — gather() returns when
165
+ # all complete (or any one raises). 10 calls take ~one
166
+ # round-trip's worth of wall-clock, not 10x.
167
+ vpss = await c.vps.list()
168
+ await asyncio.gather(*[v.reboot() for v in vpss])
169
+
170
+ # Async futures work the same way as their sync counterparts:
171
+ op = await c.vps.get(17988).snapshots.rollback("pre-update")
172
+ await op.wait(timeout=600) # AsyncOperation.wait()
173
+
174
+ invoice = await c.account.topup(amount=50, method="xmr")
175
+ await invoice.wait_until_paid(timeout=7200) # AsyncTopupInvoice
176
+
177
+ asyncio.run(main())
178
+ ```
179
+
180
+ The `impreza` CLI is **sync by design** — CLI invocations are one-shot
181
+ processes where the async-fanout speedup doesn't apply. The async path
182
+ exists for library users; everything the CLI does has a `Client.*`
183
+ equivalent. If you're integrating into a long-running async app
184
+ (FastAPI, an event consumer, a batch worker), import the SDK directly
185
+ and skip the CLI.
186
+
187
+ ## Tor routing
188
+
189
+ ```python
190
+ # Explicit
191
+ c = Client(api_key="...", api_secret="...",
192
+ proxy="socks5://127.0.0.1:9050")
193
+
194
+ # Opt-in via flag or env var
195
+ c = Client.from_env(use_tor=True) # equivalent to IMPREZA_USE_TOR=1
196
+
197
+ # Probe Tor; fall back to clearnet if not running
198
+ c = Client.from_env(auto_tor=True)
199
+ ```
200
+
201
+ Backed by `httpx[socks]`. Sync and async parity. The probe used by
202
+ `auto_tor=True` never raises — failure means clearnet.
203
+
204
+ ## Error handling
205
+
206
+ All errors subclass `ImprezaError`, so a single `except` catches
207
+ everything the SDK can throw:
208
+
209
+ ```python
210
+ from impreza import (
211
+ Client, ImprezaError,
212
+ InsufficientCredit, RateLimitExceeded, IpNotWhitelisted,
213
+ OperationFailed, TopupFailed,
214
+ )
215
+
216
+ try:
217
+ c.orders.create(product_id=12, billing_cycle="monthly", domain="example.com")
218
+ except InsufficientCredit as e:
219
+ needed = e.details.get("amount_needed")
220
+ print(f"Need ${needed} more — opening a top-up invoice")
221
+ inv = c.account.topup(amount=needed, method="btc")
222
+ print(inv.payment_url)
223
+ except RateLimitExceeded as e:
224
+ print(f"Rate limited — retry after {e.retry_after}s")
225
+ except IpNotWhitelisted:
226
+ print("Your current IP isn't on the API key's whitelist")
227
+ ```
228
+
229
+ ## Action polling
230
+
231
+ Long-running Proxmox operations return `Operation` futures:
232
+
233
+ ```python
234
+ op = c.vps.get(17988).backups.create()
235
+ print(op.uuid, op.status) # 'queued' initially
236
+ op.wait(timeout=1800, poll_interval=10)
237
+ print(op.is_success(), op.finished_at)
238
+ ```
239
+
240
+ `wait()` raises `OperationTimeout` if it doesn't finish, or
241
+ `OperationFailed` on a terminal failure state.
242
+
243
+ ## Development
244
+
245
+ ```bash
246
+ git clone https://github.com/imprezahost/impreza-devkit.git
247
+ cd impreza-devkit/sdk-python
248
+ python -m venv .venv
249
+ # Linux/macOS: source .venv/bin/activate
250
+ # Windows PowerShell: .venv\Scripts\Activate.ps1
251
+ pip install -e ".[test,dev]"
252
+
253
+ pytest # 266 unit tests + mocks
254
+ ruff check
255
+ mypy --strict impreza
256
+ ```
257
+
258
+ To run the live integration smokes:
259
+
260
+ ```bash
261
+ export IMPREZA_API_KEY="imp_..."
262
+ export IMPREZA_API_SECRET="..."
263
+ # Optional — only needed for the full topup decode smoke
264
+ export IMPREZA_TOPUP_INVOICE_ID="<id of an existing AddFunds invoice>"
265
+
266
+ pytest -v -s tests/
267
+ ```
268
+
269
+ The destructive tests (real top-up creation, real Proxmox backup
270
+ creation) are gated behind `IMPREZA_DESTRUCTIVE_TESTS=1` and skipped
271
+ by default.
272
+
273
+ ## License
274
+
275
+ MIT. See [`../LICENSE`](../LICENSE) at the repository root.