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.
Files changed (54) hide show
  1. pgdispatch/__init__.py +69 -0
  2. pgdispatch/__main__.py +5 -0
  3. pgdispatch/_version.py +1 -0
  4. pgdispatch/backends/__init__.py +6 -0
  5. pgdispatch/backends/asyncpg.py +103 -0
  6. pgdispatch/backends/base.py +142 -0
  7. pgdispatch/cli.py +420 -0
  8. pgdispatch/client.py +490 -0
  9. pgdispatch/config.py +223 -0
  10. pgdispatch/core/__init__.py +1 -0
  11. pgdispatch/core/circuit.py +94 -0
  12. pgdispatch/core/common.py +87 -0
  13. pgdispatch/core/deliveries.py +216 -0
  14. pgdispatch/core/endpoints.py +338 -0
  15. pgdispatch/core/events.py +65 -0
  16. pgdispatch/core/maintenance.py +121 -0
  17. pgdispatch/core/matching.py +76 -0
  18. pgdispatch/core/payload.py +108 -0
  19. pgdispatch/core/publisher.py +232 -0
  20. pgdispatch/core/retry.py +90 -0
  21. pgdispatch/core/secrets.py +138 -0
  22. pgdispatch/core/signing.py +141 -0
  23. pgdispatch/django/__init__.py +37 -0
  24. pgdispatch/django/apps.py +10 -0
  25. pgdispatch/django/client.py +402 -0
  26. pgdispatch/django/management/__init__.py +0 -0
  27. pgdispatch/django/management/commands/__init__.py +0 -0
  28. pgdispatch/django/management/commands/pgdispatch_worker.py +32 -0
  29. pgdispatch/django/migrations/0001_initial.py +30 -0
  30. pgdispatch/django/migrations/__init__.py +0 -0
  31. pgdispatch/errors.py +43 -0
  32. pgdispatch/fastapi/__init__.py +23 -0
  33. pgdispatch/fastapi/router.py +243 -0
  34. pgdispatch/hooks.py +80 -0
  35. pgdispatch/metrics.py +80 -0
  36. pgdispatch/migrations/0001_initial.sql +104 -0
  37. pgdispatch/migrations/__init__.py +93 -0
  38. pgdispatch/models.py +208 -0
  39. pgdispatch/py.typed +0 -0
  40. pgdispatch/security/__init__.py +1 -0
  41. pgdispatch/security/ssrf.py +141 -0
  42. pgdispatch/sqlalchemy.py +130 -0
  43. pgdispatch/worker/__init__.py +5 -0
  44. pgdispatch/worker/fetcher.py +183 -0
  45. pgdispatch/worker/ratelimit.py +47 -0
  46. pgdispatch/worker/recorder.py +435 -0
  47. pgdispatch/worker/runner.py +36 -0
  48. pgdispatch/worker/sender.py +160 -0
  49. pgdispatch/worker/worker.py +557 -0
  50. pgdispatch-0.1.0.dist-info/METADATA +329 -0
  51. pgdispatch-0.1.0.dist-info/RECORD +54 -0
  52. pgdispatch-0.1.0.dist-info/WHEEL +4 -0
  53. pgdispatch-0.1.0.dist-info/entry_points.txt +2 -0
  54. 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
+ [![CI](https://github.com/AmirSherov/pgdispatch/actions/workflows/ci.yml/badge.svg)](https://github.com/AmirSherov/pgdispatch/actions/workflows/ci.yml)
51
+ [![PyPI](https://img.shields.io/pypi/v/pgdispatch)](https://pypi.org/project/pgdispatch/)
52
+ [![Python](https://img.shields.io/pypi/pyversions/pgdispatch)](https://pypi.org/project/pgdispatch/)
53
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pgdispatch = pgdispatch.cli:main
@@ -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.