walbox 1.0.0b0__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.
- walbox-1.0.0b0/LICENSE +21 -0
- walbox-1.0.0b0/PKG-INFO +206 -0
- walbox-1.0.0b0/README.md +183 -0
- walbox-1.0.0b0/pyproject.toml +162 -0
- walbox-1.0.0b0/pyproject.toml.orig +200 -0
- walbox-1.0.0b0/walbox/__init__.py +23 -0
- walbox-1.0.0b0/walbox/abc.py +132 -0
- walbox-1.0.0b0/walbox/checkpoint.py +185 -0
- walbox-1.0.0b0/walbox/client.py +385 -0
- walbox-1.0.0b0/walbox/errors.py +115 -0
- walbox-1.0.0b0/walbox/pgoutput.py +940 -0
- walbox-1.0.0b0/walbox/protocol.py +182 -0
- walbox-1.0.0b0/walbox/transaction.py +447 -0
- walbox-1.0.0b0/walbox/transport.py +400 -0
walbox-1.0.0b0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mochama Adriano
|
|
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.
|
walbox-1.0.0b0/PKG-INFO
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: walbox
|
|
3
|
+
Version: 1.0.0b0
|
|
4
|
+
Summary: Async PostgreSQL logical-replication runtime for the transactional outbox pattern
|
|
5
|
+
Keywords: postgresql,logical-replication,outbox,asyncio,cdc,change-data-capture
|
|
6
|
+
Author: Mochama Adriano
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: psycopg>=3.2
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Project-URL: Homepage, https://github.com/mochams/walbox
|
|
19
|
+
Project-URL: Repository, https://github.com/mochams/walbox
|
|
20
|
+
Project-URL: Issues, https://github.com/mochams/walbox/issues
|
|
21
|
+
Project-URL: Author, https://github.com/mochams
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# walbox
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/walbox/)
|
|
27
|
+
[](https://github.com/mochams/walbox/actions/workflows/ci.yml)
|
|
28
|
+
[](https://www.python.org/)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
Async Python runtime for consuming PostgreSQL logical replication as a stream of
|
|
32
|
+
committed transactions, built for the transactional outbox pattern: write an outbox
|
|
33
|
+
row in the same transaction as your business data, then stream committed inserts to
|
|
34
|
+
an external system with no polling and no `LISTEN`/`NOTIFY`.
|
|
35
|
+
|
|
36
|
+
- **At-least-once delivery**: a durable local checkpoint, never silent loss
|
|
37
|
+
- **Backpressure-aware**: a slow handler can't blow up memory or starve PostgreSQL's keepalives
|
|
38
|
+
- **Reconnects and resumes** automatically from the last durable checkpoint
|
|
39
|
+
- **Graceful shutdown**: finishes in-flight work and checkpoints it before exiting
|
|
40
|
+
- **Asyncio-native**, one dependency (`psycopg`)
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install walbox
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires `libpq` available at build/run time (typically a system package, e.g.
|
|
49
|
+
`libpq-dev` on Debian/Ubuntu). To try walbox without a system `libpq`, install
|
|
50
|
+
psycopg's self-contained wheel alongside it: `pip install walbox "psycopg[binary]"`.
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
```sql
|
|
55
|
+
-- Run once, manually. walbox creates its replication slot idempotently, but
|
|
56
|
+
-- never creates or alters the publication itself (see "PostgreSQL configuration"
|
|
57
|
+
-- below).
|
|
58
|
+
CREATE TABLE outbox (
|
|
59
|
+
id BIGSERIAL PRIMARY KEY,
|
|
60
|
+
entity_type TEXT NOT NULL,
|
|
61
|
+
entity_id TEXT NOT NULL,
|
|
62
|
+
event_type TEXT NOT NULL,
|
|
63
|
+
payload JSONB NOT NULL,
|
|
64
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE PUBLICATION walbox_pub FOR TABLE outbox;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```py
|
|
71
|
+
import asyncio
|
|
72
|
+
import signal
|
|
73
|
+
|
|
74
|
+
from walbox import (
|
|
75
|
+
ChangeKind,
|
|
76
|
+
PostgresCheckpointStore,
|
|
77
|
+
ReplicationClient,
|
|
78
|
+
ReplicationOptions,
|
|
79
|
+
Transaction,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def publish_to_broker(payload: dict) -> None:
|
|
84
|
+
# Replace with your actual publish call.
|
|
85
|
+
print("publishing:", payload)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def handle(tx: Transaction) -> None:
|
|
89
|
+
for change in tx.changes:
|
|
90
|
+
if change.table != "public.outbox" or change.kind != ChangeKind.INSERT:
|
|
91
|
+
continue
|
|
92
|
+
await publish_to_broker(change.new)
|
|
93
|
+
|
|
94
|
+
await tx.checkpoint.save(tx.commit_lsn)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def main() -> None:
|
|
98
|
+
dsn = "your-postgres-dsn"
|
|
99
|
+
checkpoint_store = PostgresCheckpointStore(dsn, consumer_name="my-consumer")
|
|
100
|
+
|
|
101
|
+
options = ReplicationOptions(
|
|
102
|
+
consumer_name="my-consumer",
|
|
103
|
+
dsn=dsn,
|
|
104
|
+
slot_name="outbox_slot",
|
|
105
|
+
publication_name="walbox_pub",
|
|
106
|
+
checkpoint_store=checkpoint_store,
|
|
107
|
+
manage_checkpoint=False, # handle() checkpoints explicitly, after publishing.
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
client = ReplicationClient(options)
|
|
111
|
+
|
|
112
|
+
loop = asyncio.get_running_loop()
|
|
113
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
114
|
+
loop.add_signal_handler(sig, client.close)
|
|
115
|
+
|
|
116
|
+
await client.run(handle)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
asyncio.run(main())
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A complete, runnable version lives in [`examples/outbox.py`](examples/outbox.py),
|
|
124
|
+
including the same-transaction checkpoint pattern for a Postgres sink (see
|
|
125
|
+
"Exactly-once effects" below).
|
|
126
|
+
|
|
127
|
+
## PostgreSQL configuration
|
|
128
|
+
|
|
129
|
+
- `wal_level = logical` in `postgresql.conf` (requires a server restart).
|
|
130
|
+
- Size `max_replication_slots`/`max_wal_senders` with headroom for at least one
|
|
131
|
+
slot/sender per consumer; `max_wal_senders` should be at least as large as
|
|
132
|
+
`max_replication_slots`.
|
|
133
|
+
- The connecting role needs `REPLICATION`: `ALTER ROLE consumer_role REPLICATION;`
|
|
134
|
+
(or `CREATE ROLE ... WITH REPLICATION LOGIN;`).
|
|
135
|
+
- A `pg_hba.conf` entry granting that role access to the `replication`
|
|
136
|
+
pseudo-database, e.g. `host replication consumer_role 10.0.0.0/8 scram-sha-256`.
|
|
137
|
+
- On PostgreSQL 15+, the connecting role additionally needs `SELECT` on the
|
|
138
|
+
published tables (15 tightened this; 14 doesn't enforce it).
|
|
139
|
+
- The published table needs a usable `REPLICA IDENTITY` for `UPDATE`/`DELETE` to see
|
|
140
|
+
old-row data. A primary key (`DEFAULT`) is enough for most outbox-style tables; a
|
|
141
|
+
table with none needs `ALTER TABLE ... REPLICA IDENTITY FULL` (or `USING INDEX`).
|
|
142
|
+
- walbox creates its replication slot idempotently if missing. It does **not** create
|
|
143
|
+
the publication: `CREATE PUBLICATION` is a manual, one-time step (see Limitations).
|
|
144
|
+
|
|
145
|
+
## Exactly-once effects
|
|
146
|
+
|
|
147
|
+
walbox provides **at-least-once delivery** with a durable replay position. It does
|
|
148
|
+
**not** implement or claim end-to-end exactly-once effects. The flow: Postgres
|
|
149
|
+
transaction → outbox row → logical replication → handler → external sink.
|
|
150
|
+
Exactly-once *effects* come from combining the transactional outbox write with
|
|
151
|
+
durable checkpointing and an idempotent/deduplicating sink: either dedupe on
|
|
152
|
+
`outbox.id`, or, when the sink is itself PostgreSQL, use
|
|
153
|
+
`PostgresCheckpointStore`'s same-transaction pattern (`handle_with_atomic_checkpoint`
|
|
154
|
+
in [`examples/outbox.py`](examples/outbox.py)). If the process crashes after an
|
|
155
|
+
external publish succeeds but before the checkpoint is durable, the transaction
|
|
156
|
+
**will** be delivered again. That's intentional, not a bug.
|
|
157
|
+
|
|
158
|
+
## Failure semantics
|
|
159
|
+
|
|
160
|
+
walbox is correct if the process crashes at *any* point, whether before, during, or
|
|
161
|
+
after the handler runs, mid-checkpoint, mid-reconnect, mid-shutdown, or partway
|
|
162
|
+
through a large streamed transaction. The result is always "delivered again" or "not
|
|
163
|
+
delivered yet," never silent loss and never a torn transaction. The full
|
|
164
|
+
crash-point-by-crash-point table is in
|
|
165
|
+
[`ARCHITECTURE.md`](ARCHITECTURE.md#failure-semantics).
|
|
166
|
+
|
|
167
|
+
## Supported versions
|
|
168
|
+
|
|
169
|
+
- **PostgreSQL 14+**: the floor for protocol version 2 / `streaming 'on'`, which
|
|
170
|
+
walbox always negotiates. A pre-14 server is unsupported, not silently degraded.
|
|
171
|
+
- **Python 3.13+**: `asyncio.Queue.shutdown()`, which backpressure and graceful
|
|
172
|
+
shutdown depend on, is a 3.13 addition.
|
|
173
|
+
|
|
174
|
+
Both are deliberate v0.1 floors, not aspirations to relax later.
|
|
175
|
+
|
|
176
|
+
## Limitations
|
|
177
|
+
|
|
178
|
+
- Streamed-transaction memory isn't accounted against `max_pending_transactions`, so
|
|
179
|
+
large or numerous concurrent streamed transactions can grow memory independent of
|
|
180
|
+
that bound.
|
|
181
|
+
- No built-in metrics exporter, only a synchronous `on_metrics` callback; wiring it
|
|
182
|
+
to Prometheus/StatsD/etc. is left to the application.
|
|
183
|
+
- Strictly sequential, single-consumer handling, with no concurrent handler execution.
|
|
184
|
+
- Manual publication management: walbox never creates or alters the publication.
|
|
185
|
+
- Truncate's `CASCADE`/`RESTART IDENTITY` flags, and Type/Origin message content, are
|
|
186
|
+
decoded but never surfaced to the application.
|
|
187
|
+
|
|
188
|
+
## Status
|
|
189
|
+
|
|
190
|
+
The code is tested and correct for everything described above: 100% branch coverage,
|
|
191
|
+
including integration tests against real PostgreSQL for every failure scenario in the
|
|
192
|
+
table above. "Pre-1.0" here is about the API surface still settling, not about whether
|
|
193
|
+
it's safe to run.
|
|
194
|
+
|
|
195
|
+
Concretely: the public export list won't shrink, though construction signatures and
|
|
196
|
+
field names may still shift between pre-1.0 releases. The `Metrics` callback shape and
|
|
197
|
+
streamed-vs-non-streamed `Transaction` semantics are most likely to still change before
|
|
198
|
+
1.0. Once 1.0 ships, the stable surface follows semver.
|
|
199
|
+
|
|
200
|
+
## See also
|
|
201
|
+
|
|
202
|
+
- [`ARCHITECTURE.md`](ARCHITECTURE.md): system design, the correctness invariant, the error hierarchy
|
|
203
|
+
- [`docs/README.md`](docs/README.md): the RFCs behind each feature
|
|
204
|
+
- [`CONTRIBUTING.md`](CONTRIBUTING.md): development setup, tests, code style
|
|
205
|
+
- [`PROJECT.md`](PROJECT.md): project status and tooling rationale
|
|
206
|
+
- [`LICENSE`](LICENSE): MIT
|
walbox-1.0.0b0/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# walbox
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/walbox/)
|
|
4
|
+
[](https://github.com/mochams/walbox/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Async Python runtime for consuming PostgreSQL logical replication as a stream of
|
|
9
|
+
committed transactions, built for the transactional outbox pattern: write an outbox
|
|
10
|
+
row in the same transaction as your business data, then stream committed inserts to
|
|
11
|
+
an external system with no polling and no `LISTEN`/`NOTIFY`.
|
|
12
|
+
|
|
13
|
+
- **At-least-once delivery**: a durable local checkpoint, never silent loss
|
|
14
|
+
- **Backpressure-aware**: a slow handler can't blow up memory or starve PostgreSQL's keepalives
|
|
15
|
+
- **Reconnects and resumes** automatically from the last durable checkpoint
|
|
16
|
+
- **Graceful shutdown**: finishes in-flight work and checkpoints it before exiting
|
|
17
|
+
- **Asyncio-native**, one dependency (`psycopg`)
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install walbox
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires `libpq` available at build/run time (typically a system package, e.g.
|
|
26
|
+
`libpq-dev` on Debian/Ubuntu). To try walbox without a system `libpq`, install
|
|
27
|
+
psycopg's self-contained wheel alongside it: `pip install walbox "psycopg[binary]"`.
|
|
28
|
+
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```sql
|
|
32
|
+
-- Run once, manually. walbox creates its replication slot idempotently, but
|
|
33
|
+
-- never creates or alters the publication itself (see "PostgreSQL configuration"
|
|
34
|
+
-- below).
|
|
35
|
+
CREATE TABLE outbox (
|
|
36
|
+
id BIGSERIAL PRIMARY KEY,
|
|
37
|
+
entity_type TEXT NOT NULL,
|
|
38
|
+
entity_id TEXT NOT NULL,
|
|
39
|
+
event_type TEXT NOT NULL,
|
|
40
|
+
payload JSONB NOT NULL,
|
|
41
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE PUBLICATION walbox_pub FOR TABLE outbox;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```py
|
|
48
|
+
import asyncio
|
|
49
|
+
import signal
|
|
50
|
+
|
|
51
|
+
from walbox import (
|
|
52
|
+
ChangeKind,
|
|
53
|
+
PostgresCheckpointStore,
|
|
54
|
+
ReplicationClient,
|
|
55
|
+
ReplicationOptions,
|
|
56
|
+
Transaction,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def publish_to_broker(payload: dict) -> None:
|
|
61
|
+
# Replace with your actual publish call.
|
|
62
|
+
print("publishing:", payload)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def handle(tx: Transaction) -> None:
|
|
66
|
+
for change in tx.changes:
|
|
67
|
+
if change.table != "public.outbox" or change.kind != ChangeKind.INSERT:
|
|
68
|
+
continue
|
|
69
|
+
await publish_to_broker(change.new)
|
|
70
|
+
|
|
71
|
+
await tx.checkpoint.save(tx.commit_lsn)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def main() -> None:
|
|
75
|
+
dsn = "your-postgres-dsn"
|
|
76
|
+
checkpoint_store = PostgresCheckpointStore(dsn, consumer_name="my-consumer")
|
|
77
|
+
|
|
78
|
+
options = ReplicationOptions(
|
|
79
|
+
consumer_name="my-consumer",
|
|
80
|
+
dsn=dsn,
|
|
81
|
+
slot_name="outbox_slot",
|
|
82
|
+
publication_name="walbox_pub",
|
|
83
|
+
checkpoint_store=checkpoint_store,
|
|
84
|
+
manage_checkpoint=False, # handle() checkpoints explicitly, after publishing.
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
client = ReplicationClient(options)
|
|
88
|
+
|
|
89
|
+
loop = asyncio.get_running_loop()
|
|
90
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
91
|
+
loop.add_signal_handler(sig, client.close)
|
|
92
|
+
|
|
93
|
+
await client.run(handle)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
asyncio.run(main())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A complete, runnable version lives in [`examples/outbox.py`](examples/outbox.py),
|
|
101
|
+
including the same-transaction checkpoint pattern for a Postgres sink (see
|
|
102
|
+
"Exactly-once effects" below).
|
|
103
|
+
|
|
104
|
+
## PostgreSQL configuration
|
|
105
|
+
|
|
106
|
+
- `wal_level = logical` in `postgresql.conf` (requires a server restart).
|
|
107
|
+
- Size `max_replication_slots`/`max_wal_senders` with headroom for at least one
|
|
108
|
+
slot/sender per consumer; `max_wal_senders` should be at least as large as
|
|
109
|
+
`max_replication_slots`.
|
|
110
|
+
- The connecting role needs `REPLICATION`: `ALTER ROLE consumer_role REPLICATION;`
|
|
111
|
+
(or `CREATE ROLE ... WITH REPLICATION LOGIN;`).
|
|
112
|
+
- A `pg_hba.conf` entry granting that role access to the `replication`
|
|
113
|
+
pseudo-database, e.g. `host replication consumer_role 10.0.0.0/8 scram-sha-256`.
|
|
114
|
+
- On PostgreSQL 15+, the connecting role additionally needs `SELECT` on the
|
|
115
|
+
published tables (15 tightened this; 14 doesn't enforce it).
|
|
116
|
+
- The published table needs a usable `REPLICA IDENTITY` for `UPDATE`/`DELETE` to see
|
|
117
|
+
old-row data. A primary key (`DEFAULT`) is enough for most outbox-style tables; a
|
|
118
|
+
table with none needs `ALTER TABLE ... REPLICA IDENTITY FULL` (or `USING INDEX`).
|
|
119
|
+
- walbox creates its replication slot idempotently if missing. It does **not** create
|
|
120
|
+
the publication: `CREATE PUBLICATION` is a manual, one-time step (see Limitations).
|
|
121
|
+
|
|
122
|
+
## Exactly-once effects
|
|
123
|
+
|
|
124
|
+
walbox provides **at-least-once delivery** with a durable replay position. It does
|
|
125
|
+
**not** implement or claim end-to-end exactly-once effects. The flow: Postgres
|
|
126
|
+
transaction → outbox row → logical replication → handler → external sink.
|
|
127
|
+
Exactly-once *effects* come from combining the transactional outbox write with
|
|
128
|
+
durable checkpointing and an idempotent/deduplicating sink: either dedupe on
|
|
129
|
+
`outbox.id`, or, when the sink is itself PostgreSQL, use
|
|
130
|
+
`PostgresCheckpointStore`'s same-transaction pattern (`handle_with_atomic_checkpoint`
|
|
131
|
+
in [`examples/outbox.py`](examples/outbox.py)). If the process crashes after an
|
|
132
|
+
external publish succeeds but before the checkpoint is durable, the transaction
|
|
133
|
+
**will** be delivered again. That's intentional, not a bug.
|
|
134
|
+
|
|
135
|
+
## Failure semantics
|
|
136
|
+
|
|
137
|
+
walbox is correct if the process crashes at *any* point, whether before, during, or
|
|
138
|
+
after the handler runs, mid-checkpoint, mid-reconnect, mid-shutdown, or partway
|
|
139
|
+
through a large streamed transaction. The result is always "delivered again" or "not
|
|
140
|
+
delivered yet," never silent loss and never a torn transaction. The full
|
|
141
|
+
crash-point-by-crash-point table is in
|
|
142
|
+
[`ARCHITECTURE.md`](ARCHITECTURE.md#failure-semantics).
|
|
143
|
+
|
|
144
|
+
## Supported versions
|
|
145
|
+
|
|
146
|
+
- **PostgreSQL 14+**: the floor for protocol version 2 / `streaming 'on'`, which
|
|
147
|
+
walbox always negotiates. A pre-14 server is unsupported, not silently degraded.
|
|
148
|
+
- **Python 3.13+**: `asyncio.Queue.shutdown()`, which backpressure and graceful
|
|
149
|
+
shutdown depend on, is a 3.13 addition.
|
|
150
|
+
|
|
151
|
+
Both are deliberate v0.1 floors, not aspirations to relax later.
|
|
152
|
+
|
|
153
|
+
## Limitations
|
|
154
|
+
|
|
155
|
+
- Streamed-transaction memory isn't accounted against `max_pending_transactions`, so
|
|
156
|
+
large or numerous concurrent streamed transactions can grow memory independent of
|
|
157
|
+
that bound.
|
|
158
|
+
- No built-in metrics exporter, only a synchronous `on_metrics` callback; wiring it
|
|
159
|
+
to Prometheus/StatsD/etc. is left to the application.
|
|
160
|
+
- Strictly sequential, single-consumer handling, with no concurrent handler execution.
|
|
161
|
+
- Manual publication management: walbox never creates or alters the publication.
|
|
162
|
+
- Truncate's `CASCADE`/`RESTART IDENTITY` flags, and Type/Origin message content, are
|
|
163
|
+
decoded but never surfaced to the application.
|
|
164
|
+
|
|
165
|
+
## Status
|
|
166
|
+
|
|
167
|
+
The code is tested and correct for everything described above: 100% branch coverage,
|
|
168
|
+
including integration tests against real PostgreSQL for every failure scenario in the
|
|
169
|
+
table above. "Pre-1.0" here is about the API surface still settling, not about whether
|
|
170
|
+
it's safe to run.
|
|
171
|
+
|
|
172
|
+
Concretely: the public export list won't shrink, though construction signatures and
|
|
173
|
+
field names may still shift between pre-1.0 releases. The `Metrics` callback shape and
|
|
174
|
+
streamed-vs-non-streamed `Transaction` semantics are most likely to still change before
|
|
175
|
+
1.0. Once 1.0 ships, the stable surface follows semver.
|
|
176
|
+
|
|
177
|
+
## See also
|
|
178
|
+
|
|
179
|
+
- [`ARCHITECTURE.md`](ARCHITECTURE.md): system design, the correctness invariant, the error hierarchy
|
|
180
|
+
- [`docs/README.md`](docs/README.md): the RFCs behind each feature
|
|
181
|
+
- [`CONTRIBUTING.md`](CONTRIBUTING.md): development setup, tests, code style
|
|
182
|
+
- [`PROJECT.md`](PROJECT.md): project status and tooling rationale
|
|
183
|
+
- [`LICENSE`](LICENSE): MIT
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.0,<0.13.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "walbox"
|
|
7
|
+
version = "1.0.0b0"
|
|
8
|
+
description = "Async PostgreSQL logical-replication runtime for the transactional outbox pattern"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = [
|
|
14
|
+
"postgresql",
|
|
15
|
+
"logical-replication",
|
|
16
|
+
"outbox",
|
|
17
|
+
"asyncio",
|
|
18
|
+
"cdc",
|
|
19
|
+
"change-data-capture",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Framework :: AsyncIO",
|
|
27
|
+
"Topic :: Database",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = ["psycopg>=3.2"]
|
|
31
|
+
|
|
32
|
+
[[project.authors]]
|
|
33
|
+
name = "Mochama Adriano"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/mochams/walbox"
|
|
37
|
+
Repository = "https://github.com/mochams/walbox"
|
|
38
|
+
Issues = "https://github.com/mochams/walbox/issues"
|
|
39
|
+
Author = "https://github.com/mochams"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"cryptography>=50.0.0",
|
|
44
|
+
"prek>=0.4.13",
|
|
45
|
+
"psycopg[binary]>=3.2",
|
|
46
|
+
"pyrefly>=1.2.0",
|
|
47
|
+
"pytest>=9.1.1",
|
|
48
|
+
"pytest-asyncio>=1.4.0",
|
|
49
|
+
"pytest-cov>=7.1.0",
|
|
50
|
+
"pytest-timeout>=2.4.0",
|
|
51
|
+
"pytest-xdist>=3.8.0",
|
|
52
|
+
"ruff>=0.16.1",
|
|
53
|
+
"testcontainers[postgres]>=4.9",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.uv.build-backend]
|
|
57
|
+
module-name = "walbox"
|
|
58
|
+
module-root = "."
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
extend-exclude = ["docs"]
|
|
62
|
+
line-length = 88
|
|
63
|
+
indent-width = 4
|
|
64
|
+
target-version = "py313"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
preview = true
|
|
68
|
+
select = ["ALL"]
|
|
69
|
+
ignore = [
|
|
70
|
+
"assert",
|
|
71
|
+
"mutable-class-default",
|
|
72
|
+
"collapsible-if",
|
|
73
|
+
"missing-copyright-notice",
|
|
74
|
+
"missing-return-type-undocumented-public-function",
|
|
75
|
+
"missing-return-type-private-function",
|
|
76
|
+
"no-self-use",
|
|
77
|
+
"unused-method-argument",
|
|
78
|
+
"undefined-local-with-import-star-usage",
|
|
79
|
+
"class-as-data-structure",
|
|
80
|
+
]
|
|
81
|
+
exclude = ["tests/*.py"]
|
|
82
|
+
fixable = ["ALL"]
|
|
83
|
+
unfixable = []
|
|
84
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint.isort]
|
|
87
|
+
force-single-line = true
|
|
88
|
+
case-sensitive = true
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.pydocstyle]
|
|
91
|
+
convention = "google"
|
|
92
|
+
|
|
93
|
+
[tool.ruff.lint.pylint]
|
|
94
|
+
max-args = 7
|
|
95
|
+
|
|
96
|
+
[tool.ruff.format]
|
|
97
|
+
preview = true
|
|
98
|
+
exclude = [
|
|
99
|
+
"COM812",
|
|
100
|
+
"ISC001",
|
|
101
|
+
]
|
|
102
|
+
quote-style = "double"
|
|
103
|
+
indent-style = "space"
|
|
104
|
+
skip-magic-trailing-comma = false
|
|
105
|
+
line-ending = "auto"
|
|
106
|
+
docstring-code-format = true
|
|
107
|
+
docstring-code-line-length = 88
|
|
108
|
+
|
|
109
|
+
[tool.pyrefly]
|
|
110
|
+
min-severity = "warn"
|
|
111
|
+
project-excludes = [
|
|
112
|
+
"tests",
|
|
113
|
+
"docs",
|
|
114
|
+
"examples",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
[tool.pytest.ini_options]
|
|
118
|
+
asyncio_mode = "auto"
|
|
119
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
120
|
+
addopts = [
|
|
121
|
+
"--strict-markers",
|
|
122
|
+
"--cov=walbox",
|
|
123
|
+
"--cov=examples",
|
|
124
|
+
"--cov-fail-under=100",
|
|
125
|
+
"--cov-report=term-missing",
|
|
126
|
+
"--durations=10",
|
|
127
|
+
"--timeout=10",
|
|
128
|
+
"-n 0",
|
|
129
|
+
]
|
|
130
|
+
testpaths = ["tests"]
|
|
131
|
+
markers = ["postgres: requires a real PostgreSQL instance, started via testcontainers (make test-integration)"]
|
|
132
|
+
log_cli = "false"
|
|
133
|
+
log_cli_level = "ERROR"
|
|
134
|
+
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
|
135
|
+
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
|
|
136
|
+
|
|
137
|
+
[tool.coverage.run]
|
|
138
|
+
branch = true
|
|
139
|
+
disable_warnings = [
|
|
140
|
+
"include-ignored",
|
|
141
|
+
"no-data-collected",
|
|
142
|
+
]
|
|
143
|
+
parallel = true
|
|
144
|
+
concurrency = [
|
|
145
|
+
"multiprocessing",
|
|
146
|
+
"thread",
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
[tool.coverage.report]
|
|
150
|
+
exclude_also = [
|
|
151
|
+
"def __repr__",
|
|
152
|
+
"def __str__",
|
|
153
|
+
'if self\.debug',
|
|
154
|
+
"raise AssertionError",
|
|
155
|
+
"raise NotImplementedError",
|
|
156
|
+
"if 0:",
|
|
157
|
+
"if __name__ == .__main__.:",
|
|
158
|
+
'@(abc\.)?abstractmethod',
|
|
159
|
+
'^\s*\.\.\.$',
|
|
160
|
+
"if TYPE_CHECKING:",
|
|
161
|
+
]
|
|
162
|
+
skip_covered = "true"
|