posthook-python 1.0.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.
- posthook_python-1.0.0/.gitignore +9 -0
- posthook_python-1.0.0/LICENSE +21 -0
- posthook_python-1.0.0/PKG-INFO +513 -0
- posthook_python-1.0.0/README.md +489 -0
- posthook_python-1.0.0/RELEASING.md +64 -0
- posthook_python-1.0.0/pyproject.toml +53 -0
- posthook_python-1.0.0/src/posthook/__init__.py +73 -0
- posthook_python-1.0.0/src/posthook/_client.py +124 -0
- posthook_python-1.0.0/src/posthook/_errors.py +120 -0
- posthook_python-1.0.0/src/posthook/_http.py +224 -0
- posthook_python-1.0.0/src/posthook/_models.py +162 -0
- posthook_python-1.0.0/src/posthook/_resources/__init__.py +10 -0
- posthook_python-1.0.0/src/posthook/_resources/_hooks.py +517 -0
- posthook_python-1.0.0/src/posthook/_resources/_signatures.py +169 -0
- posthook_python-1.0.0/src/posthook/_version.py +1 -0
- posthook_python-1.0.0/src/posthook/py.typed +0 -0
- posthook_python-1.0.0/tests/conftest.py +77 -0
- posthook_python-1.0.0/tests/test_client.py +87 -0
- posthook_python-1.0.0/tests/test_errors.py +111 -0
- posthook_python-1.0.0/tests/test_hooks.py +431 -0
- posthook_python-1.0.0/tests/test_signatures.py +206 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Posthook
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: posthook-python
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The official Python client library for the Posthook API
|
|
5
|
+
Project-URL: Homepage, https://posthook.io
|
|
6
|
+
Project-URL: Documentation, https://posthook.io/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/posthook/posthook-python
|
|
8
|
+
Author-email: Posthook <support@posthook.io>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx<1,>=0.25.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# posthook
|
|
26
|
+
|
|
27
|
+
The official Python client library for the [Posthook](https://posthook.io) API -- schedule webhooks and deliver them reliably.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install posthook-python
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Requirements:** Python 3.9+. Only dependency is [httpx](https://www.python-httpx.org/).
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import posthook
|
|
41
|
+
|
|
42
|
+
client = posthook.Posthook("pk_...")
|
|
43
|
+
|
|
44
|
+
# Schedule a webhook 5 minutes from now
|
|
45
|
+
hook = client.hooks.schedule(
|
|
46
|
+
path="/webhooks/user-created",
|
|
47
|
+
post_in="5m",
|
|
48
|
+
data={"userId": "123", "event": "user.created"},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
print(hook.id) # UUID
|
|
52
|
+
print(hook.status) # "pending"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## How It Works
|
|
56
|
+
|
|
57
|
+
Your Posthook project has a **domain** configured in the [dashboard](https://posthook.io) (e.g., `webhook.example.com`). When you schedule a hook, you specify a **path** (e.g., `/webhooks/user-created`). At the scheduled time, Posthook delivers the hook by POSTing to the full URL (`https://webhook.example.com/webhooks/user-created`) with your data payload and signature headers.
|
|
58
|
+
|
|
59
|
+
## Authentication
|
|
60
|
+
|
|
61
|
+
You can find your API key under **Project Settings** in the [Posthook dashboard](https://posthook.io). Pass it directly to the constructor, or set the `POSTHOOK_API_KEY` environment variable:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Explicit API key
|
|
65
|
+
client = posthook.Posthook("pk_...")
|
|
66
|
+
|
|
67
|
+
# From environment variable
|
|
68
|
+
client = posthook.Posthook() # reads POSTHOOK_API_KEY
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For webhook signature verification, also provide a signing key:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
client = posthook.Posthook("pk_...", signing_key="ph_sk_...")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The signing key can also be set via the `POSTHOOK_SIGNING_KEY` environment variable.
|
|
78
|
+
|
|
79
|
+
## Scheduling Hooks
|
|
80
|
+
|
|
81
|
+
Three mutually exclusive scheduling modes are available. You must provide exactly one of `post_in`, `post_at`, or `post_at_local`.
|
|
82
|
+
|
|
83
|
+
### Relative delay (`post_in`)
|
|
84
|
+
|
|
85
|
+
Schedule after a relative delay. Accepts `s` (seconds), `m` (minutes), `h` (hours), or `d` (days):
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
hook = client.hooks.schedule(
|
|
89
|
+
path="/webhooks/send-reminder",
|
|
90
|
+
post_in="30m",
|
|
91
|
+
data={"userId": "123"},
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Absolute UTC time (`post_at`)
|
|
96
|
+
|
|
97
|
+
Schedule at an exact UTC time. Accepts `datetime` objects or ISO 8601 strings:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from datetime import datetime, timedelta, timezone
|
|
101
|
+
|
|
102
|
+
# Using a datetime object (automatically converted to UTC)
|
|
103
|
+
hook = client.hooks.schedule(
|
|
104
|
+
path="/webhooks/send-reminder",
|
|
105
|
+
post_at=datetime.now(timezone.utc) + timedelta(hours=1),
|
|
106
|
+
data={"userId": "123"},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Using an ISO string
|
|
110
|
+
hook = client.hooks.schedule(
|
|
111
|
+
path="/webhooks/send-reminder",
|
|
112
|
+
post_at="2026-06-15T10:00:00Z",
|
|
113
|
+
data={"userId": "123"},
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Local time with timezone (`post_at_local`)
|
|
118
|
+
|
|
119
|
+
Schedule at a local time. Posthook handles DST transitions automatically:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
hook = client.hooks.schedule(
|
|
123
|
+
path="/webhooks/daily-digest",
|
|
124
|
+
post_at_local="2026-03-01T09:00:00",
|
|
125
|
+
timezone="America/New_York",
|
|
126
|
+
data={"userId": "123"},
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Custom retry configuration
|
|
131
|
+
|
|
132
|
+
Override your project's default retry behavior for a specific hook:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
hook = client.hooks.schedule(
|
|
136
|
+
path="/webhooks/critical",
|
|
137
|
+
post_in="1m",
|
|
138
|
+
data={"orderId": "456"},
|
|
139
|
+
retry_override=posthook.HookRetryOverride(
|
|
140
|
+
min_retries=10,
|
|
141
|
+
delay_secs=15,
|
|
142
|
+
strategy="exponential",
|
|
143
|
+
backoff_factor=2.0,
|
|
144
|
+
max_delay_secs=3600,
|
|
145
|
+
jitter=True,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Managing Hooks
|
|
151
|
+
|
|
152
|
+
### Get a hook
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
hook = client.hooks.get("hook-uuid")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### List hooks
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
hooks = client.hooks.list(status=posthook.STATUS_FAILED, limit=50)
|
|
162
|
+
print(f"Found {len(hooks)} hooks")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
All list parameters are optional:
|
|
166
|
+
|
|
167
|
+
| Parameter | Description |
|
|
168
|
+
|-----------|-------------|
|
|
169
|
+
| `status` | Filter by status: `"pending"`, `"retry"`, `"completed"`, `"failed"` |
|
|
170
|
+
| `limit` | Max results per page |
|
|
171
|
+
| `sort_by` | Sort field (e.g., `"createdAt"`, `"postAt"`) |
|
|
172
|
+
| `sort_order` | `"ASC"` or `"DESC"` |
|
|
173
|
+
| `post_at_before` | Filter hooks scheduled before this time (ISO string) |
|
|
174
|
+
| `post_at_after` | Cursor: hooks scheduled after this time (ISO string) |
|
|
175
|
+
| `created_at_before` | Filter hooks created before this time (ISO string) |
|
|
176
|
+
| `created_at_after` | Filter hooks created after this time (ISO string) |
|
|
177
|
+
|
|
178
|
+
### Cursor-based pagination
|
|
179
|
+
|
|
180
|
+
Use `post_at_after` as a cursor. After each page, advance it to the last hook's `post_at`:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
limit = 100
|
|
184
|
+
cursor = None
|
|
185
|
+
while True:
|
|
186
|
+
hooks = client.hooks.list(status="failed", limit=limit, post_at_after=cursor)
|
|
187
|
+
for hook in hooks:
|
|
188
|
+
print(hook.id, hook.failure_error)
|
|
189
|
+
|
|
190
|
+
if len(hooks) < limit:
|
|
191
|
+
break # last page
|
|
192
|
+
cursor = hooks[-1].post_at.isoformat()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Auto-paginating iterator (`list_all`)
|
|
196
|
+
|
|
197
|
+
For convenience, `list_all` yields every matching hook across all pages automatically:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
for hook in client.hooks.list_all(status="failed"):
|
|
201
|
+
process(hook)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The async client returns an async iterator:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
async for hook in client.hooks.list_all(status="failed"):
|
|
208
|
+
await process(hook)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Delete a hook
|
|
212
|
+
|
|
213
|
+
Idempotent -- returns `None` on both success and 404 (already delivered or gone):
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
client.hooks.delete("hook-uuid")
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Bulk Operations
|
|
220
|
+
|
|
221
|
+
Three bulk operations are available, each supporting by-IDs or by-filter:
|
|
222
|
+
|
|
223
|
+
- **Retry** -- Re-attempts delivery for failed hooks
|
|
224
|
+
- **Replay** -- Re-delivers completed hooks (useful for reprocessing)
|
|
225
|
+
- **Cancel** -- Cancels pending hooks before delivery
|
|
226
|
+
|
|
227
|
+
### By IDs
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
result = client.hooks.bulk.retry(["id-1", "id-2", "id-3"])
|
|
231
|
+
print(f"Retried {result.affected} hooks")
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### By filter
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
result = client.hooks.bulk.cancel_by_filter(
|
|
238
|
+
start_time="2026-02-01T00:00:00Z",
|
|
239
|
+
end_time="2026-02-22T00:00:00Z",
|
|
240
|
+
limit=500,
|
|
241
|
+
endpoint_key="/webhooks/deprecated",
|
|
242
|
+
)
|
|
243
|
+
print(f"Cancelled {result.affected} hooks")
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
All six methods:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
# By IDs
|
|
250
|
+
client.hooks.bulk.retry(hook_ids)
|
|
251
|
+
client.hooks.bulk.replay(hook_ids)
|
|
252
|
+
client.hooks.bulk.cancel(hook_ids)
|
|
253
|
+
|
|
254
|
+
# By filter
|
|
255
|
+
client.hooks.bulk.retry_by_filter(start_time, end_time, limit, ...)
|
|
256
|
+
client.hooks.bulk.replay_by_filter(start_time, end_time, limit, ...)
|
|
257
|
+
client.hooks.bulk.cancel_by_filter(start_time, end_time, limit, ...)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Filter methods also accept optional `endpoint_key` and `sequence_id` keyword arguments.
|
|
261
|
+
|
|
262
|
+
## Verifying Webhook Signatures
|
|
263
|
+
|
|
264
|
+
When Posthook delivers a hook to your endpoint, it includes signature headers for verification. Use `parse_delivery` to verify and parse the delivery.
|
|
265
|
+
|
|
266
|
+
**Important:** You must pass the **raw request body** (bytes or string), not a parsed JSON object.
|
|
267
|
+
|
|
268
|
+
### Flask
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
from flask import Flask, request
|
|
272
|
+
import posthook
|
|
273
|
+
|
|
274
|
+
app = Flask(__name__)
|
|
275
|
+
client = posthook.Posthook("pk_...", signing_key="ph_sk_...")
|
|
276
|
+
|
|
277
|
+
@app.route("/webhooks/user-created", methods=["POST"])
|
|
278
|
+
def handle_webhook():
|
|
279
|
+
try:
|
|
280
|
+
delivery = client.signatures.parse_delivery(
|
|
281
|
+
body=request.get_data(),
|
|
282
|
+
headers=dict(request.headers),
|
|
283
|
+
)
|
|
284
|
+
except posthook.SignatureVerificationError:
|
|
285
|
+
return "invalid signature", 401
|
|
286
|
+
|
|
287
|
+
print(delivery.hook_id) # from Posthook-Id header
|
|
288
|
+
print(delivery.path) # "/webhooks/user-created"
|
|
289
|
+
print(delivery.data) # your custom data payload
|
|
290
|
+
print(delivery.post_at) # when it was scheduled
|
|
291
|
+
print(delivery.posted_at) # when it was delivered
|
|
292
|
+
|
|
293
|
+
return "", 200
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Django
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from django.http import HttpResponse
|
|
300
|
+
import posthook
|
|
301
|
+
|
|
302
|
+
client = posthook.Posthook("pk_...", signing_key="ph_sk_...")
|
|
303
|
+
|
|
304
|
+
def handle_webhook(request):
|
|
305
|
+
try:
|
|
306
|
+
delivery = client.signatures.parse_delivery(
|
|
307
|
+
body=request.body,
|
|
308
|
+
headers=dict(request.headers),
|
|
309
|
+
)
|
|
310
|
+
except posthook.SignatureVerificationError:
|
|
311
|
+
return HttpResponse(status=401)
|
|
312
|
+
|
|
313
|
+
print(delivery.hook_id)
|
|
314
|
+
print(delivery.data)
|
|
315
|
+
|
|
316
|
+
return HttpResponse(status=200)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### FastAPI
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
from fastapi import FastAPI, Request, Response
|
|
323
|
+
import posthook
|
|
324
|
+
|
|
325
|
+
app = FastAPI()
|
|
326
|
+
client = posthook.Posthook("pk_...", signing_key="ph_sk_...")
|
|
327
|
+
|
|
328
|
+
@app.post("/webhooks/user-created")
|
|
329
|
+
async def handle_webhook(request: Request):
|
|
330
|
+
body = await request.body()
|
|
331
|
+
try:
|
|
332
|
+
delivery = client.signatures.parse_delivery(
|
|
333
|
+
body=body,
|
|
334
|
+
headers=dict(request.headers),
|
|
335
|
+
)
|
|
336
|
+
except posthook.SignatureVerificationError:
|
|
337
|
+
return Response(status_code=401)
|
|
338
|
+
|
|
339
|
+
print(delivery.hook_id)
|
|
340
|
+
print(delivery.data)
|
|
341
|
+
|
|
342
|
+
return Response(status_code=200)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Custom tolerance
|
|
346
|
+
|
|
347
|
+
By default, signatures older than 5 minutes are rejected. You can override this:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
delivery = client.signatures.parse_delivery(
|
|
351
|
+
body=raw_body,
|
|
352
|
+
headers=headers,
|
|
353
|
+
tolerance=600, # 10 minutes, in seconds
|
|
354
|
+
)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Error Handling
|
|
358
|
+
|
|
359
|
+
All API errors extend `PosthookError` and can be caught with `isinstance` or `except`:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
import posthook
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
hook = client.hooks.get("hook-id")
|
|
366
|
+
except posthook.RateLimitError:
|
|
367
|
+
print("Rate limited, retry later")
|
|
368
|
+
except posthook.AuthenticationError:
|
|
369
|
+
print("Invalid API key")
|
|
370
|
+
except posthook.NotFoundError:
|
|
371
|
+
print("Hook not found")
|
|
372
|
+
except posthook.PosthookError as err:
|
|
373
|
+
print(f"API error: {err.message} (status={err.status_code})")
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
| Error class | HTTP Status | Code |
|
|
377
|
+
|---|---|---|
|
|
378
|
+
| `BadRequestError` | 400 | `bad_request` |
|
|
379
|
+
| `AuthenticationError` | 401 | `authentication_error` |
|
|
380
|
+
| `ForbiddenError` | 403 | `forbidden` |
|
|
381
|
+
| `NotFoundError` | 404 | `not_found` |
|
|
382
|
+
| `PayloadTooLargeError` | 413 | `payload_too_large` |
|
|
383
|
+
| `RateLimitError` | 429 | `rate_limit_exceeded` |
|
|
384
|
+
| `InternalServerError` | 5xx | `internal_error` |
|
|
385
|
+
| `PosthookConnectionError` | -- | `connection_error` |
|
|
386
|
+
| `SignatureVerificationError` | -- | `signature_verification_error` |
|
|
387
|
+
|
|
388
|
+
## Configuration
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
client = posthook.Posthook(
|
|
392
|
+
"pk_...",
|
|
393
|
+
base_url="https://api.staging.posthook.io",
|
|
394
|
+
timeout=60,
|
|
395
|
+
signing_key="ph_sk_...",
|
|
396
|
+
)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
| Option | Description | Default |
|
|
400
|
+
|--------|-------------|---------|
|
|
401
|
+
| `api_key` | Your Posthook API key | `POSTHOOK_API_KEY` env var |
|
|
402
|
+
| `base_url` | Custom API base URL | `https://api.posthook.io` |
|
|
403
|
+
| `timeout` | Request timeout in seconds | `30` |
|
|
404
|
+
| `signing_key` | Signing key for webhook verification | `POSTHOOK_SIGNING_KEY` env var |
|
|
405
|
+
| `http_client` | Custom `httpx.Client` instance | -- |
|
|
406
|
+
|
|
407
|
+
## Quota Info
|
|
408
|
+
|
|
409
|
+
After scheduling a hook, quota information is available on the returned `Hook` object:
|
|
410
|
+
|
|
411
|
+
```python
|
|
412
|
+
hook = client.hooks.schedule(path="/test", post_in="5m")
|
|
413
|
+
|
|
414
|
+
if hook.quota:
|
|
415
|
+
print(f"Limit: {hook.quota.limit}")
|
|
416
|
+
print(f"Usage: {hook.quota.usage}")
|
|
417
|
+
print(f"Remaining: {hook.quota.remaining}")
|
|
418
|
+
print(f"Resets at: {hook.quota.resets_at}")
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Async Client
|
|
422
|
+
|
|
423
|
+
The `AsyncPosthook` client provides an identical API -- just `await` each call:
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
import posthook
|
|
427
|
+
|
|
428
|
+
async with posthook.AsyncPosthook("pk_...") as client:
|
|
429
|
+
hook = await client.hooks.schedule(path="/test", post_in="5m")
|
|
430
|
+
print(hook.id)
|
|
431
|
+
|
|
432
|
+
hooks = await client.hooks.list(status="pending")
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Both the sync and async clients support context managers for automatic cleanup:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
# Sync
|
|
439
|
+
with posthook.Posthook("pk_...") as client:
|
|
440
|
+
hook = client.hooks.schedule(path="/test", post_in="5m")
|
|
441
|
+
|
|
442
|
+
# Async
|
|
443
|
+
async with posthook.AsyncPosthook("pk_...") as client:
|
|
444
|
+
hook = await client.hooks.schedule(path="/test", post_in="5m")
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
You can also call `close()` / `await close()` manually if you prefer.
|
|
448
|
+
|
|
449
|
+
## Debug Logging
|
|
450
|
+
|
|
451
|
+
The SDK logs all requests via Python's `logging` module under the `"posthook"` logger. Enable it to see request details:
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
import logging
|
|
455
|
+
|
|
456
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Example output:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
DEBUG:posthook:POST /v1/hooks -> 200 (0.153s)
|
|
463
|
+
DEBUG:posthook:GET /v1/hooks -> 200 (0.089s)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## Advanced
|
|
467
|
+
|
|
468
|
+
### Proxy support
|
|
469
|
+
|
|
470
|
+
Pass a custom `httpx.Client` configured with a proxy:
|
|
471
|
+
|
|
472
|
+
```python
|
|
473
|
+
import httpx
|
|
474
|
+
import posthook
|
|
475
|
+
|
|
476
|
+
http_client = httpx.Client(proxy="http://proxy.example.com:8080")
|
|
477
|
+
client = posthook.Posthook("pk_...", http_client=http_client)
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Custom CA certificates
|
|
481
|
+
|
|
482
|
+
```python
|
|
483
|
+
import httpx
|
|
484
|
+
import posthook
|
|
485
|
+
|
|
486
|
+
http_client = httpx.Client(verify="/path/to/custom-ca-bundle.crt")
|
|
487
|
+
client = posthook.Posthook("pk_...", http_client=http_client)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Custom httpx client
|
|
491
|
+
|
|
492
|
+
For full control over HTTP behavior, provide your own `httpx.Client` (sync) or `httpx.AsyncClient` (async). The SDK will add its authentication headers automatically:
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
import httpx
|
|
496
|
+
import posthook
|
|
497
|
+
|
|
498
|
+
http_client = httpx.Client(
|
|
499
|
+
timeout=60,
|
|
500
|
+
verify=True,
|
|
501
|
+
proxy="http://proxy.example.com:8080",
|
|
502
|
+
limits=httpx.Limits(max_connections=20),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
client = posthook.Posthook("pk_...", http_client=http_client)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
When you provide a custom client, the SDK does **not** close it on `client.close()` -- you are responsible for its lifecycle.
|
|
509
|
+
|
|
510
|
+
## Requirements
|
|
511
|
+
|
|
512
|
+
- Python 3.9+
|
|
513
|
+
- [httpx](https://www.python-httpx.org/) >= 0.25.0
|