pgdispatch 0.1.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.
- pgdispatch/__init__.py +69 -0
- pgdispatch/__main__.py +5 -0
- pgdispatch/_version.py +1 -0
- pgdispatch/backends/__init__.py +6 -0
- pgdispatch/backends/asyncpg.py +103 -0
- pgdispatch/backends/base.py +142 -0
- pgdispatch/cli.py +420 -0
- pgdispatch/client.py +490 -0
- pgdispatch/config.py +223 -0
- pgdispatch/core/__init__.py +1 -0
- pgdispatch/core/circuit.py +94 -0
- pgdispatch/core/common.py +87 -0
- pgdispatch/core/deliveries.py +216 -0
- pgdispatch/core/endpoints.py +338 -0
- pgdispatch/core/events.py +65 -0
- pgdispatch/core/maintenance.py +121 -0
- pgdispatch/core/matching.py +76 -0
- pgdispatch/core/payload.py +108 -0
- pgdispatch/core/publisher.py +232 -0
- pgdispatch/core/retry.py +90 -0
- pgdispatch/core/secrets.py +138 -0
- pgdispatch/core/signing.py +141 -0
- pgdispatch/django/__init__.py +37 -0
- pgdispatch/django/apps.py +10 -0
- pgdispatch/django/client.py +402 -0
- pgdispatch/django/management/__init__.py +0 -0
- pgdispatch/django/management/commands/__init__.py +0 -0
- pgdispatch/django/management/commands/pgdispatch_worker.py +32 -0
- pgdispatch/django/migrations/0001_initial.py +30 -0
- pgdispatch/django/migrations/__init__.py +0 -0
- pgdispatch/errors.py +43 -0
- pgdispatch/fastapi/__init__.py +23 -0
- pgdispatch/fastapi/router.py +243 -0
- pgdispatch/hooks.py +80 -0
- pgdispatch/metrics.py +80 -0
- pgdispatch/migrations/0001_initial.sql +104 -0
- pgdispatch/migrations/__init__.py +93 -0
- pgdispatch/models.py +208 -0
- pgdispatch/py.typed +0 -0
- pgdispatch/security/__init__.py +1 -0
- pgdispatch/security/ssrf.py +141 -0
- pgdispatch/sqlalchemy.py +130 -0
- pgdispatch/worker/__init__.py +5 -0
- pgdispatch/worker/fetcher.py +183 -0
- pgdispatch/worker/ratelimit.py +47 -0
- pgdispatch/worker/recorder.py +435 -0
- pgdispatch/worker/runner.py +36 -0
- pgdispatch/worker/sender.py +160 -0
- pgdispatch/worker/worker.py +557 -0
- pgdispatch-0.1.0.dist-info/METADATA +329 -0
- pgdispatch-0.1.0.dist-info/RECORD +54 -0
- pgdispatch-0.1.0.dist-info/WHEEL +4 -0
- pgdispatch-0.1.0.dist-info/entry_points.txt +2 -0
- pgdispatch-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pgdispatch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reliable webhook delivery for Python apps, using only the PostgreSQL you already have.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AmirSherov/pgdispatch
|
|
6
|
+
Project-URL: Documentation, https://github.com/AmirSherov/pgdispatch/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/AmirSherov/pgdispatch
|
|
8
|
+
Project-URL: Issues, https://github.com/AmirSherov/pgdispatch/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/AmirSherov/pgdispatch/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Amir Sherov <sher4mir@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: asyncpg,django,fastapi,outbox,postgresql,sqlalchemy,standard-webhooks,transactional-outbox,webhooks
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: FastAPI
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Database
|
|
27
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Requires-Dist: aiohttp>=3.9
|
|
32
|
+
Requires-Dist: asyncpg>=0.29
|
|
33
|
+
Requires-Dist: prometheus-client>=0.17
|
|
34
|
+
Requires-Dist: pydantic-settings>=2.2
|
|
35
|
+
Requires-Dist: pydantic>=2.6
|
|
36
|
+
Provides-Extra: django
|
|
37
|
+
Requires-Dist: django>=4.2; extra == 'django'
|
|
38
|
+
Provides-Extra: encryption
|
|
39
|
+
Requires-Dist: cryptography>=41; extra == 'encryption'
|
|
40
|
+
Provides-Extra: fastapi
|
|
41
|
+
Requires-Dist: fastapi>=0.110; extra == 'fastapi'
|
|
42
|
+
Provides-Extra: sqlalchemy
|
|
43
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.20; extra == 'sqlalchemy'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# pgdispatch
|
|
47
|
+
|
|
48
|
+
**Reliable webhook delivery for Python apps. It needs only the PostgreSQL you already have.**
|
|
49
|
+
|
|
50
|
+
[](https://github.com/AmirSherov/pgdispatch/actions/workflows/ci.yml)
|
|
51
|
+
[](https://pypi.org/project/pgdispatch/)
|
|
52
|
+
[](https://pypi.org/project/pgdispatch/)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
|
|
55
|
+
You publish an event in the same database transaction as your business change.
|
|
56
|
+
pgdispatch then delivers it to every subscribed endpoint. Each request is signed
|
|
57
|
+
according to [Standard Webhooks](https://www.standardwebhooks.com/). Failed
|
|
58
|
+
requests are retried for about 27 hours, a circuit breaker protects endpoints
|
|
59
|
+
that are down, and every attempt is logged so you can replay it. You don't need
|
|
60
|
+
Redis, Kafka, Celery or a separate webhook service.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
async with session.begin():
|
|
64
|
+
order.status = "paid"
|
|
65
|
+
await webhooks.publish(session, "order.paid", {"id": order.id, "amount": 15000})
|
|
66
|
+
# Committed: the event will be delivered. Rolled back: it never existed.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Why
|
|
70
|
+
|
|
71
|
+
Sending webhooks naively loses events and duplicates them:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
order.status = "paid"
|
|
75
|
+
await session.commit()
|
|
76
|
+
await httpx.post(customer_url, json=payload) # the process dies here, and the event is gone
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **Dual write.** If the process crashes between `commit` and `post`, the order
|
|
80
|
+
is saved but the webhook is lost forever. If you swap the two lines, a
|
|
81
|
+
customer can be told about an order that was never saved.
|
|
82
|
+
- **No retries, no back-off.** A `503` or a timeout from the receiver is the end of it.
|
|
83
|
+
- **Dead endpoints slow everyone down.** A customer whose server is down for a day keeps tying up your workers.
|
|
84
|
+
- **No authenticity.** Receivers can't prove that a request came from you.
|
|
85
|
+
- **No audit trail.** Support can't answer "did customer X get event Y, and what did they reply?"
|
|
86
|
+
|
|
87
|
+
pgdispatch solves these problems with a **transactional outbox** that lives in
|
|
88
|
+
your database. A small async **worker** reads it with `FOR UPDATE SKIP LOCKED`
|
|
89
|
+
and wakes up instantly on `LISTEN/NOTIFY`.
|
|
90
|
+
|
|
91
|
+
## Features
|
|
92
|
+
|
|
93
|
+
- **Transactional outbox**: `publish()` joins your transaction. Works with SQLAlchemy 2.0 (async), Django and asyncpg.
|
|
94
|
+
- **At-least-once delivery**: workers can be killed at any moment (`kill -9` included) and nothing is lost.
|
|
95
|
+
- **Standard Webhooks signatures**: receivers verify requests with official SDKs in any language. Secrets can be rotated with zero downtime.
|
|
96
|
+
- **Retries** with jitter and `Retry-After` support. The default schedule has 8 attempts over about 27 hours and can be changed.
|
|
97
|
+
- **Per-endpoint circuit breaker** with growing cool-downs. Endpoints that keep failing are disabled automatically.
|
|
98
|
+
- **Ordered delivery** per `ordering_key` when you need it; full parallelism when you don't.
|
|
99
|
+
- **Idempotent publishing** with `idempotency_key`.
|
|
100
|
+
- **Multi-tenant**: endpoints belong to an `owner_id`, and events never cross owners.
|
|
101
|
+
- **SSRF protection**: private addresses are blocked, the connection is pinned to the checked IP (which stops DNS rebinding), and redirects are never followed.
|
|
102
|
+
- **Observability**: Prometheus metrics, a Grafana dashboard, alert rules, a CLI and a full attempt log.
|
|
103
|
+
- **Ready-made FastAPI router** that lets your customers manage their endpoints.
|
|
104
|
+
- **Typed** (`mypy --strict`), with a small set of core dependencies: `asyncpg`, `aiohttp`, `pydantic`, `prometheus-client`.
|
|
105
|
+
|
|
106
|
+
## Installation
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install "pgdispatch[sqlalchemy]" # FastAPI / SQLAlchemy projects
|
|
110
|
+
pip install "pgdispatch[django]" # Django projects
|
|
111
|
+
pip install pgdispatch # plain asyncpg
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
You need Python 3.10+ and PostgreSQL 13+.
|
|
115
|
+
|
|
116
|
+
## Quick start: FastAPI + SQLAlchemy
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from fastapi import FastAPI
|
|
120
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
121
|
+
|
|
122
|
+
from pgdispatch import Webhooks
|
|
123
|
+
from pgdispatch.sqlalchemy import SQLAlchemyBackend
|
|
124
|
+
|
|
125
|
+
engine = create_async_engine("postgresql+asyncpg://app:secret@localhost/app")
|
|
126
|
+
webhooks = Webhooks(SQLAlchemyBackend(engine))
|
|
127
|
+
|
|
128
|
+
# Create the schema on start-up and deliver webhooks from inside the app.
|
|
129
|
+
app = FastAPI(lifespan=webhooks.lifespan(migrate=True))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.post("/orders/{order_id}/pay")
|
|
133
|
+
async def pay(order_id: int, session: SessionDep) -> None:
|
|
134
|
+
async with session.begin():
|
|
135
|
+
order = await session.get(Order, order_id)
|
|
136
|
+
order.status = "paid"
|
|
137
|
+
await webhooks.publish(
|
|
138
|
+
session,
|
|
139
|
+
"order.paid",
|
|
140
|
+
{"id": order.id, "amount": order.amount},
|
|
141
|
+
owner_id=order.merchant_id, # deliver only to this merchant's endpoints
|
|
142
|
+
ordering_key=f"order:{order.id}", # optional: keep this order's events in sequence
|
|
143
|
+
idempotency_key=f"order.paid:{order.id}", # optional: publish at most once
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Then register an endpoint. You would normally do this from your product's API:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
endpoint = await webhooks.endpoints.create(
|
|
151
|
+
"https://merchant.example.com/webhooks",
|
|
152
|
+
event_types=["order.*"],
|
|
153
|
+
owner_id="merchant_42",
|
|
154
|
+
)
|
|
155
|
+
endpoint.secret # "whsec_..." — show it to the customer once
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
As traffic grows, take the worker out of the web process and scale it on its own:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pgdispatch worker --dsn postgresql://app:secret@localhost/app --concurrency 100
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Quick start: Django
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# settings.py
|
|
168
|
+
INSTALLED_APPS = [..., "pgdispatch.django"]
|
|
169
|
+
PGDISPATCH = {"concurrency": 50} # optional; any WebhooksConfig field
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from django.db import transaction
|
|
174
|
+
from pgdispatch.django import webhooks
|
|
175
|
+
|
|
176
|
+
with transaction.atomic():
|
|
177
|
+
order.status = "shipped"
|
|
178
|
+
order.save()
|
|
179
|
+
webhooks.publish("order.shipped", {"id": order.id}, owner_id=order.shop_id)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
python manage.py migrate
|
|
184
|
+
python manage.py pgdispatch_worker --concurrency 50
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## What receivers get
|
|
188
|
+
|
|
189
|
+
```http
|
|
190
|
+
POST /webhooks HTTP/1.1
|
|
191
|
+
content-type: application/json
|
|
192
|
+
webhook-id: 01a1985f-e246-725c-a639-369d5fd0a21b
|
|
193
|
+
webhook-timestamp: 1793527200
|
|
194
|
+
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
|
|
195
|
+
|
|
196
|
+
{"type":"order.paid","timestamp":"2026-11-01T10:00:00.123Z","data":{"id":42,"amount":15000}}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Receivers can verify requests with any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries)
|
|
200
|
+
or with pgdispatch itself:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from pgdispatch import verify_signature
|
|
204
|
+
|
|
205
|
+
verify_signature(secret, request.headers, await request.body()) # raises InvalidSignatureError
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
[`examples/receivers`](examples/receivers) has complete, dependency-free receivers in
|
|
209
|
+
Python, Node.js and Go. `webhook-id` is the same on every retry, so receivers
|
|
210
|
+
should use it to deduplicate.
|
|
211
|
+
|
|
212
|
+
## Guarantees
|
|
213
|
+
|
|
214
|
+
| Guarantee | Yes / No | Details |
|
|
215
|
+
| --- | --- | --- |
|
|
216
|
+
| An event from a rolled-back transaction is never sent | **yes** | The event and your data are written in one transaction. |
|
|
217
|
+
| A committed event is never lost | **yes** | If a worker dies, its lease expires and another worker takes the delivery over. |
|
|
218
|
+
| Exactly-once delivery | **no** | A worker can die after the receiver answered `200` but before the result was stored. Deduplicate by `webhook-id`. |
|
|
219
|
+
| Ordering | **with `ordering_key`** | Deliveries with the same key go to an endpoint one at a time, in publish order. Without a key, events can arrive in any order. |
|
|
220
|
+
| Delivery while the receiver is down | **for about 27 hours** | Then the delivery is marked `failed`, and you can retry or replay it at any time. |
|
|
221
|
+
| No extra infrastructure | **yes** | PostgreSQL 13+ only. |
|
|
222
|
+
|
|
223
|
+
The test suite proves these guarantees against real PostgreSQL and real worker processes
|
|
224
|
+
([`tests/chaos`](tests/chaos/test_guarantees.py)):
|
|
225
|
+
|
|
226
|
+
- [x] A rolled-back transaction creates 0 deliveries.
|
|
227
|
+
- [x] `kill -9` of a worker with 1,000 deliveries in flight: after a restart all 1,000 arrive and none are lost.
|
|
228
|
+
- [x] 4 worker processes deliver 10,000 events, each exactly once.
|
|
229
|
+
- [x] The endpoint goes down, the circuit breaker opens, and after recovery every event arrives.
|
|
230
|
+
- [x] 1,000 events with one `ordering_key`, handled by two competing workers, arrive strictly in order.
|
|
231
|
+
|
|
232
|
+
## How it works
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
your transaction pgdispatch worker(s)
|
|
236
|
+
┌──────────────────────────┐ ┌───────────────────────────────────────┐
|
|
237
|
+
│ UPDATE orders ... │ │ 1. LISTEN pgdispatch_new / poll │
|
|
238
|
+
│ publish(): │ COMMIT │ 2. claim due deliveries │
|
|
239
|
+
│ INSERT event │ ───────▶ │ FOR UPDATE SKIP LOCKED + lease │
|
|
240
|
+
│ INSERT deliveries │ NOTIFY │ 3. sign + POST (aiohttp, SSRF-safe) │
|
|
241
|
+
│ (one per subscriber) │ │ 4. record attempts, schedule retries, │
|
|
242
|
+
└──────────────────────────┘ │ update circuit breakers (batched) │
|
|
243
|
+
└───────────────────────────────────────┘
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
All state is kept in the `pgdispatch` schema: `endpoints`, `events`,
|
|
247
|
+
`deliveries`, `attempts` and `workers`. A worker claims deliveries with a
|
|
248
|
+
short **lease** instead of holding a lock while it sends a request, so any
|
|
249
|
+
worker can be stopped at any time. Run as many workers as you like; they
|
|
250
|
+
coordinate only through row locks. See [docs/architecture.md](docs/architecture.md).
|
|
251
|
+
|
|
252
|
+
## Performance
|
|
253
|
+
|
|
254
|
+
These numbers come from one desktop machine (i5-14400F, PostgreSQL 17 in
|
|
255
|
+
Docker), with a fast local receiver:
|
|
256
|
+
|
|
257
|
+
| workers | deliveries/s | p50 latency | p99 latency |
|
|
258
|
+
| ---: | ---: | ---: | ---: |
|
|
259
|
+
| 1 | 1,083 | 6 ms | 11 ms |
|
|
260
|
+
| 2 | 2,707 | 7 ms | 14 ms |
|
|
261
|
+
| 4 | 4,995 | 6 ms | 14 ms |
|
|
262
|
+
|
|
263
|
+
Latency is measured from `publish()` until the request reaches the receiver.
|
|
264
|
+
See [benchmarks](benchmarks) to reproduce the run on your own hardware.
|
|
265
|
+
|
|
266
|
+
## Comparison
|
|
267
|
+
|
|
268
|
+
| | Form | Infrastructure | Transactional outbox | Signatures | License |
|
|
269
|
+
| --- | --- | --- | --- | --- | --- |
|
|
270
|
+
| **pgdispatch** | Python library | PostgreSQL | yes, in your transaction | Standard Webhooks | MIT |
|
|
271
|
+
| [Svix](https://github.com/svix/svix-webhooks) | standalone server (Rust) | PostgreSQL, optional Redis | no, called over HTTP | Standard Webhooks | MIT |
|
|
272
|
+
| [Convoy](https://github.com/frain-dev/convoy) | standalone server (Go) | PostgreSQL + Redis | no, called over HTTP | own scheme | Elastic License 2.0 |
|
|
273
|
+
| [Hook0](https://github.com/hook0/hook0) | standalone server (Rust) | PostgreSQL | no, called over HTTP | own scheme | SSPL v1 |
|
|
274
|
+
|
|
275
|
+
Standalone servers are a good fit when many services in many languages need to
|
|
276
|
+
send webhooks. pgdispatch is for Python teams who want webhooks to be part of
|
|
277
|
+
their own application and database, with no extra service to deploy, monitor and
|
|
278
|
+
secure. (Licenses were checked in November 2026.)
|
|
279
|
+
|
|
280
|
+
## Operations
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
pgdispatch status # queue size, delivery lag, open circuit breakers
|
|
284
|
+
pgdispatch deliveries --status failed --since 1h
|
|
285
|
+
pgdispatch deliveries show 18342 # every attempt with the receiver's response
|
|
286
|
+
pgdispatch retry 18342 # retry one delivery
|
|
287
|
+
pgdispatch retry --endpoint <id> # retry all failed deliveries of an endpoint
|
|
288
|
+
pgdispatch replay --event <id> [--endpoint <id>] # deliver an event again
|
|
289
|
+
pgdispatch endpoints list --owner merchant_42
|
|
290
|
+
pgdispatch endpoints enable <id> # re-enable and reset the circuit breaker
|
|
291
|
+
pgdispatch cleanup --older-than 14d
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The same operations are available from Python (`webhooks.deliveries.retry(...)`,
|
|
295
|
+
`webhooks.events.replay(...)`, …). Metrics such as
|
|
296
|
+
`pgdispatch_oldest_pending_age_seconds` (delivery lag) are exported to Prometheus.
|
|
297
|
+
[`contrib/`](contrib) contains a Grafana dashboard and alert rules.
|
|
298
|
+
|
|
299
|
+
## Documentation
|
|
300
|
+
|
|
301
|
+
- [Getting started](docs/getting-started.md)
|
|
302
|
+
- [Concepts](docs/concepts.md): events, endpoints, deliveries, attempts, owners, ordering
|
|
303
|
+
- [Publishing](docs/publishing.md) and [managing endpoints](docs/endpoints.md)
|
|
304
|
+
- [Receiving webhooks](docs/receiving.md): signature verification in any language
|
|
305
|
+
- [Running workers](docs/workers.md): deployment, scaling, graceful shutdown
|
|
306
|
+
- [Configuration](docs/configuration.md)
|
|
307
|
+
- [Reliability and guarantees](docs/guarantees.md)
|
|
308
|
+
- [Security](docs/security.md): SSRF, secrets, data retention
|
|
309
|
+
- [Observability](docs/observability.md): metrics, logs, hooks, CLI
|
|
310
|
+
- [Architecture](docs/architecture.md)
|
|
311
|
+
|
|
312
|
+
## Try it in five minutes
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
git clone https://github.com/AmirSherov/pgdispatch && cd pgdispatch
|
|
316
|
+
docker compose up --build
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
This starts PostgreSQL, an example shop, a worker, a receiver, Prometheus and
|
|
320
|
+
Grafana. Follow [examples/fastapi_shop](examples/fastapi_shop) to send your first webhook.
|
|
321
|
+
|
|
322
|
+
## Contributing
|
|
323
|
+
|
|
324
|
+
Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
325
|
+
Please report security problems privately, as described in [SECURITY.md](SECURITY.md).
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
[MIT](LICENSE) © Amir Sherov. pgdispatch is free for commercial use.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
pgdispatch/__init__.py,sha256=vFGdklbF3khFik5vR8kNB-y5DJ3-Q9nlUmGDDZpahYU,1817
|
|
2
|
+
pgdispatch/__main__.py,sha256=HgZaMLK1-_4IO2S6Dsf0W-vx0pYFC7MbhMUfRgEi8PM,62
|
|
3
|
+
pgdispatch/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
4
|
+
pgdispatch/cli.py,sha256=Xq6TCZlx4luCSqqhsm8rmzrVMdd0xiQ8CSIZUSkt8rw,16467
|
|
5
|
+
pgdispatch/client.py,sha256=nQTLSCdg5ax-qEhIXARfGmR-iGJBKW0lMuV_d3Uuef0,18254
|
|
6
|
+
pgdispatch/config.py,sha256=EhyNqLxK3PbNptasdcJuMnM8aOXYWd5ZSAxQGUFm9_k,8559
|
|
7
|
+
pgdispatch/errors.py,sha256=40BVj1_Gxm0O_nU86tLj6CRKuVsETQYJhWP2qm6PkEg,1233
|
|
8
|
+
pgdispatch/hooks.py,sha256=iJeVooh1fOnqZJZA8FzDivJAzHojaMqJIkgcwLO9zyo,2788
|
|
9
|
+
pgdispatch/metrics.py,sha256=sCXWLhMUIlkphsqjJAj-aGhj6d7SUT8Lrx7xaUd0NEU,2899
|
|
10
|
+
pgdispatch/models.py,sha256=WbdlbCbKylABDOian95Z5lhDVlBi9wjJMJMMBUH0exQ,6362
|
|
11
|
+
pgdispatch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
pgdispatch/sqlalchemy.py,sha256=2ZW-eDZyMPmc5B6kev61kRi7zEVHuFj_cuJ6Qr0a_hg,4592
|
|
13
|
+
pgdispatch/backends/__init__.py,sha256=BAUe6IrISHgzJF7UMlctj3LFWMtDuLJhoaYj2Cu8T3g,215
|
|
14
|
+
pgdispatch/backends/asyncpg.py,sha256=bIaVMXLORm1eeG0zoLPVKONTqvOL9kh1ZDAbQwExMt8,3828
|
|
15
|
+
pgdispatch/backends/base.py,sha256=Bv-GJ7FuDDGTMKJa3l6rXNPSarXTicKJaWs9BRQVyTc,4640
|
|
16
|
+
pgdispatch/core/__init__.py,sha256=rS467N64gQ4LAsyfDw_dPSrnYIe0zWQ5kxiK974pHLQ,100
|
|
17
|
+
pgdispatch/core/circuit.py,sha256=5nXzJ3AxsNJwH6t55OFWorJmyvCInrf8X69JF1Dnuf8,3509
|
|
18
|
+
pgdispatch/core/common.py,sha256=ccU1oyuSM87gkrFF797-YXqupIUhpB7aYdSh2Bar8Eo,2545
|
|
19
|
+
pgdispatch/core/deliveries.py,sha256=6hCuA29KeHwziu2SeewzjehFgUfH0RYulTBO2kzAwfs,7966
|
|
20
|
+
pgdispatch/core/endpoints.py,sha256=G5gUg9vQJpKhGB-gUzzJOJQyHOoCBrbsQboRMz1j0mY,12262
|
|
21
|
+
pgdispatch/core/events.py,sha256=Yv9Zdt896NOl5nfRw7hShlxwrwghPgJvcDFp9EAjs2Y,2186
|
|
22
|
+
pgdispatch/core/maintenance.py,sha256=S1b0DRLzBZKf9HSZJrg5M1k29HB56aduo5FT6w14vPQ,4947
|
|
23
|
+
pgdispatch/core/matching.py,sha256=bcjcVanFr6AcwVxpHDII5zvuXpuqTAuhiwe9lmN-2I0,2686
|
|
24
|
+
pgdispatch/core/payload.py,sha256=S1yIF9_GMuwH-5bknD-7WAhGoYQjUFizzvuWNU1HKX4,3749
|
|
25
|
+
pgdispatch/core/publisher.py,sha256=ndET8FC9w7YH_97TlK414dNq1VB3mx8Sd1-8UvPp6hQ,8996
|
|
26
|
+
pgdispatch/core/retry.py,sha256=wR640owo18LqkrzhCZ61SMSpnl6Kn70AKP66kQEVXPg,2872
|
|
27
|
+
pgdispatch/core/secrets.py,sha256=iLD4ihHM0eQ70iN1yeTEzjHgrHGEFUCp4RseAd9DR4Y,5182
|
|
28
|
+
pgdispatch/core/signing.py,sha256=fO3Kdv_4faEXoQWZv2YCXeSN0RACCdWNECoXnFK28bA,4775
|
|
29
|
+
pgdispatch/django/__init__.py,sha256=hF_QNJ8a1kT7xN3Y_c9PoKzlyRWVlmiv1cE9T5TXfNQ,1040
|
|
30
|
+
pgdispatch/django/apps.py,sha256=mIPxQ-gGvBVv1QpPxc4moyAR3fYkQGYoTtYJkSADCF0,261
|
|
31
|
+
pgdispatch/django/client.py,sha256=u-OCPS9bYeYzoDq1jVM_Uvkka2NbQlhcKbut0eK8O58,14429
|
|
32
|
+
pgdispatch/django/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
pgdispatch/django/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
pgdispatch/django/management/commands/pgdispatch_worker.py,sha256=N3uQfRkrhoVBh4WnW1ZCV0Z8XteErRUM2ObIVclz69o,1251
|
|
35
|
+
pgdispatch/django/migrations/0001_initial.py,sha256=rKrewmBHnhJjT9MPtkQI_zkqaaW5EOkGdEhoEuqDYKU,841
|
|
36
|
+
pgdispatch/django/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
pgdispatch/fastapi/__init__.py,sha256=Mc8Kkl0R-TSyExfNVByvm7fwZAwb045YwR1QwSmIZps,767
|
|
38
|
+
pgdispatch/fastapi/router.py,sha256=wBX231JwxB-SzOFysbKjOoqpLPbq5rX5PT0mdRWQJhM,9186
|
|
39
|
+
pgdispatch/migrations/0001_initial.sql,sha256=es0G6vgv0lUEN51-dcpSJESPe4z8VIr4Gf0M1cv4EVI,4437
|
|
40
|
+
pgdispatch/migrations/__init__.py,sha256=b8L3V3okqomtE-cCcBUkF9b_2S-znY4mit5p8vPBeWQ,3287
|
|
41
|
+
pgdispatch/security/__init__.py,sha256=Ie2CQKVtg20pGpthfZ9g7O_2jPqZBTGZg4ktP41XfkQ,31
|
|
42
|
+
pgdispatch/security/ssrf.py,sha256=Qs5NLZBEFe0p-fOJtaZtsCNqRYx_bYV6Ld-Q8QKAlH8,5424
|
|
43
|
+
pgdispatch/worker/__init__.py,sha256=NQ5O_YVB7sHN_FcYsEKizQcdAGp_QX96l3Z4x3qXMAY,105
|
|
44
|
+
pgdispatch/worker/fetcher.py,sha256=zgt-4ScznehPsOacFZyWct3MOjfK8CUAGRNL5P6uwQk,6243
|
|
45
|
+
pgdispatch/worker/ratelimit.py,sha256=Sabz39W6HU9UUxu1HgyjIzD8VccrW4TjSv2hbTzcCM0,1839
|
|
46
|
+
pgdispatch/worker/recorder.py,sha256=BP2z_CIAviL2aaOLWkVKi2sKIIDdL7fCZ0roqWRu9Ec,17626
|
|
47
|
+
pgdispatch/worker/runner.py,sha256=S9GwoUJ7dzsqscpN1OQzbZWksN_AlMLiwxU2Fsd1_TU,1255
|
|
48
|
+
pgdispatch/worker/sender.py,sha256=HJD0Rn0BhnBqx1oOKfAQUZXJ-obT9qKLMDC58dUxjUA,5527
|
|
49
|
+
pgdispatch/worker/worker.py,sha256=EbNUNrT4q7UORrHMgQyzQfnKnqN_hE0y75UknL90JnE,23483
|
|
50
|
+
pgdispatch-0.1.0.dist-info/METADATA,sha256=pJok3YL97U_dpNO4ZkG1ocp74vuq7Q1PhNXg6RY4Wwg,15199
|
|
51
|
+
pgdispatch-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
52
|
+
pgdispatch-0.1.0.dist-info/entry_points.txt,sha256=Q2a9NvGGKzF5YRsYlb8IBBakUGduCzPS1PxfOtS-rj4,51
|
|
53
|
+
pgdispatch-0.1.0.dist-info/licenses/LICENSE,sha256=x8EV1yotzzzKMz5znm7DmbzddFWa6HVvGkfAVVy0V5g,1068
|
|
54
|
+
pgdispatch-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amir Sherov
|
|
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.
|