event-forge 1.1.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.
- event_forge-1.1.0/.gitignore +75 -0
- event_forge-1.1.0/PKG-INFO +226 -0
- event_forge-1.1.0/README.md +190 -0
- event_forge-1.1.0/pyproject.toml +74 -0
- event_forge-1.1.0/src/event_forge/__init__.py +65 -0
- event_forge-1.1.0/src/event_forge/client.py +113 -0
- event_forge-1.1.0/src/event_forge/config.py +75 -0
- event_forge-1.1.0/src/event_forge/consumers/__init__.py +4 -0
- event_forge-1.1.0/src/event_forge/consumers/aio_pika_consumer.py +145 -0
- event_forge-1.1.0/src/event_forge/errors/__init__.py +3 -0
- event_forge-1.1.0/src/event_forge/errors/duplicate_message_error.py +3 -0
- event_forge-1.1.0/src/event_forge/errors/processing_error.py +3 -0
- event_forge-1.1.0/src/event_forge/events.py +72 -0
- event_forge-1.1.0/src/event_forge/models/__init__.py +4 -0
- event_forge-1.1.0/src/event_forge/models/enums.py +13 -0
- event_forge-1.1.0/src/event_forge/models/inbox_message.py +27 -0
- event_forge-1.1.0/src/event_forge/models/outbox_message.py +32 -0
- event_forge-1.1.0/src/event_forge/publishers/__init__.py +4 -0
- event_forge-1.1.0/src/event_forge/publishers/aio_pika_publisher.py +136 -0
- event_forge-1.1.0/src/event_forge/publishers/interfaces.py +51 -0
- event_forge-1.1.0/src/event_forge/repositories/__init__.py +3 -0
- event_forge-1.1.0/src/event_forge/repositories/interfaces.py +92 -0
- event_forge-1.1.0/src/event_forge/repositories/sqlalchemy/__init__.py +11 -0
- event_forge-1.1.0/src/event_forge/repositories/sqlalchemy/entities.py +66 -0
- event_forge-1.1.0/src/event_forge/repositories/sqlalchemy/inbox_repository.py +174 -0
- event_forge-1.1.0/src/event_forge/repositories/sqlalchemy/outbox_repository.py +222 -0
- event_forge-1.1.0/src/event_forge/services/__init__.py +4 -0
- event_forge-1.1.0/src/event_forge/services/inbox_service.py +238 -0
- event_forge-1.1.0/src/event_forge/services/outbox_service.py +209 -0
- event_forge-1.1.0/tests/__init__.py +1 -0
- event_forge-1.1.0/tests/test_errors.py +8 -0
- event_forge-1.1.0/tests/test_models.py +19 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.idea/
|
|
2
|
+
.claude/tasks/
|
|
3
|
+
.claude/knowledge-base/
|
|
4
|
+
|
|
5
|
+
# Dependencies
|
|
6
|
+
node_modules/
|
|
7
|
+
.pnpm-store/
|
|
8
|
+
|
|
9
|
+
# Build outputs
|
|
10
|
+
dist/
|
|
11
|
+
*.tsbuildinfo
|
|
12
|
+
|
|
13
|
+
# Backup files
|
|
14
|
+
*.bak
|
|
15
|
+
*.bak*
|
|
16
|
+
|
|
17
|
+
# Test coverage
|
|
18
|
+
coverage/
|
|
19
|
+
.nyc_output/
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.vscode/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
*~
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Logs
|
|
32
|
+
*.log
|
|
33
|
+
npm-debug.log*
|
|
34
|
+
pnpm-debug.log*
|
|
35
|
+
|
|
36
|
+
# Cache
|
|
37
|
+
.turbo/
|
|
38
|
+
.cache/
|
|
39
|
+
.eslintcache
|
|
40
|
+
|
|
41
|
+
# Environment
|
|
42
|
+
.env
|
|
43
|
+
.env.local
|
|
44
|
+
.env.*.local
|
|
45
|
+
|
|
46
|
+
# Python
|
|
47
|
+
__pycache__/
|
|
48
|
+
*.py[cod]
|
|
49
|
+
*$py.class
|
|
50
|
+
*.so
|
|
51
|
+
.Python
|
|
52
|
+
build/
|
|
53
|
+
develop-eggs/
|
|
54
|
+
dist/
|
|
55
|
+
downloads/
|
|
56
|
+
eggs/
|
|
57
|
+
.eggs/
|
|
58
|
+
lib/
|
|
59
|
+
lib64/
|
|
60
|
+
parts/
|
|
61
|
+
sdist/
|
|
62
|
+
var/
|
|
63
|
+
wheels/
|
|
64
|
+
*.egg-info/
|
|
65
|
+
.installed.cfg
|
|
66
|
+
*.egg
|
|
67
|
+
MANIFEST
|
|
68
|
+
.pytest_cache/
|
|
69
|
+
.coverage
|
|
70
|
+
htmlcov/
|
|
71
|
+
.tox/
|
|
72
|
+
.venv/
|
|
73
|
+
venv/
|
|
74
|
+
ENV/
|
|
75
|
+
env/
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: event-forge
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Universal Inbox-Outbox Pattern Library for Python - reliable message delivery in distributed systems
|
|
5
|
+
Project-URL: Homepage, https://github.com/codeforprod/Event-Forge
|
|
6
|
+
Project-URL: Documentation, https://github.com/codeforprod/Event-Forge/tree/main/packages/python
|
|
7
|
+
Project-URL: Repository, https://github.com/codeforprod/Event-Forge
|
|
8
|
+
Project-URL: Issues, https://github.com/codeforprod/Event-Forge/issues
|
|
9
|
+
Author-email: ProdForCode <dev@prodforcode.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: inbox,message,mongodb,outbox,pattern,postgresql,rabbitmq,sqlalchemy,transactional
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
23
|
+
Requires-Dist: sqlalchemy[asyncio]<3.0.0,>=2.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: black>=23.7.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy>=1.5.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.0.285; extra == 'dev'
|
|
31
|
+
Provides-Extra: mongodb
|
|
32
|
+
Requires-Dist: motor<4.0.0,>=3.3.0; extra == 'mongodb'
|
|
33
|
+
Provides-Extra: rabbitmq
|
|
34
|
+
Requires-Dist: aio-pika<10.0.0,>=9.0.0; extra == 'rabbitmq'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# Event-Forge (Python)
|
|
38
|
+
|
|
39
|
+
Python implementation of the Universal Inbox-Outbox Pattern for reliable message delivery in distributed systems.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install event-forge
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
With RabbitMQ support:
|
|
48
|
+
```bash
|
|
49
|
+
pip install event-forge[rabbitmq]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
With MongoDB support:
|
|
53
|
+
```bash
|
|
54
|
+
pip install event-forge[mongodb]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Python >=3.10
|
|
60
|
+
- SQLAlchemy >= 2.0 (async support)
|
|
61
|
+
- Pydantic >= 2.0
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### PostgreSQL + SQLAlchemy
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from event_forge import OutboxService, CreateOutboxMessageDto, OutboxConfig
|
|
69
|
+
from event_forge.repositories.sqlalchemy import (
|
|
70
|
+
SQLAlchemyOutboxRepository,
|
|
71
|
+
Base,
|
|
72
|
+
)
|
|
73
|
+
from event_forge.publishers.aio_pika_publisher import AioPikaPublisher
|
|
74
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
75
|
+
|
|
76
|
+
# Setup database
|
|
77
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
|
|
78
|
+
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
79
|
+
|
|
80
|
+
# Create tables
|
|
81
|
+
async with engine.begin() as conn:
|
|
82
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
83
|
+
|
|
84
|
+
# Setup repository and publisher
|
|
85
|
+
repository = SQLAlchemyOutboxRepository(session_factory)
|
|
86
|
+
publisher = AioPikaPublisher(
|
|
87
|
+
url="amqp://guest:guest@localhost/",
|
|
88
|
+
exchange_name="events",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Create service
|
|
92
|
+
outbox = OutboxService(
|
|
93
|
+
repository=repository,
|
|
94
|
+
publisher=publisher,
|
|
95
|
+
config=OutboxConfig(polling_interval=1.0, batch_size=10),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Connect publisher and start polling
|
|
99
|
+
await publisher.connect()
|
|
100
|
+
outbox.start_polling()
|
|
101
|
+
|
|
102
|
+
# Create message in transaction
|
|
103
|
+
async def create_user_event(session):
|
|
104
|
+
dto = CreateOutboxMessageDto(
|
|
105
|
+
aggregate_type="User",
|
|
106
|
+
aggregate_id="user-123",
|
|
107
|
+
event_type="user.created",
|
|
108
|
+
payload={"email": "user@example.com", "name": "John Doe"},
|
|
109
|
+
)
|
|
110
|
+
return await outbox.create_message(dto, session)
|
|
111
|
+
|
|
112
|
+
await outbox.with_transaction(create_user_event)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Fire-and-Forget Client
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from event_forge import EventForgeAgentClient, OutboxConfig
|
|
119
|
+
from event_forge.repositories.sqlalchemy import SQLAlchemyOutboxRepository
|
|
120
|
+
from event_forge.publishers.aio_pika_publisher import AioPikaPublisher
|
|
121
|
+
|
|
122
|
+
client = EventForgeAgentClient(
|
|
123
|
+
repository=SQLAlchemyOutboxRepository(session_factory),
|
|
124
|
+
publisher=AioPikaPublisher(url="amqp://localhost/", exchange_name="events"),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
await client.start()
|
|
128
|
+
|
|
129
|
+
# Fire-and-forget
|
|
130
|
+
await client.fire(
|
|
131
|
+
aggregate_type="Call",
|
|
132
|
+
aggregate_id="call-123",
|
|
133
|
+
event_type="call.completed",
|
|
134
|
+
payload={"duration": 120, "status": "success"},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
await client.stop()
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Inbox Pattern
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from event_forge import InboxService, CreateInboxMessageDto, InboxConfig
|
|
144
|
+
from event_forge.repositories.sqlalchemy import SQLAlchemyInboxRepository
|
|
145
|
+
|
|
146
|
+
# Setup
|
|
147
|
+
repository = SQLAlchemyInboxRepository(session_factory)
|
|
148
|
+
inbox = InboxService(
|
|
149
|
+
repository=repository,
|
|
150
|
+
config=InboxConfig(enable_retry=True),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Register handler
|
|
154
|
+
async def handle_order(message):
|
|
155
|
+
print(f"Processing order: {message.payload}")
|
|
156
|
+
|
|
157
|
+
inbox.register_handler("order.placed", handle_order)
|
|
158
|
+
|
|
159
|
+
# Process incoming message (with automatic deduplication)
|
|
160
|
+
dto = CreateInboxMessageDto(
|
|
161
|
+
message_id="ext-msg-123",
|
|
162
|
+
source="external-system",
|
|
163
|
+
event_type="order.placed",
|
|
164
|
+
payload={"order_id": "order-456", "amount": 99.99},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
await inbox.receive_message(dto)
|
|
168
|
+
|
|
169
|
+
# Start retry polling for failed messages
|
|
170
|
+
inbox.start_retry_polling()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### RabbitMQ Consumer
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from event_forge.consumers import AioPikaConsumer
|
|
177
|
+
|
|
178
|
+
consumer = AioPikaConsumer(
|
|
179
|
+
url="amqp://guest:guest@localhost/",
|
|
180
|
+
inbox_service=inbox,
|
|
181
|
+
queue_name="my-service-inbox",
|
|
182
|
+
exchange_name="events",
|
|
183
|
+
routing_keys=["Order.*", "User.*"],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
await consumer.start()
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Architecture
|
|
190
|
+
|
|
191
|
+
Matches the TypeScript implementation with full wire format compatibility:
|
|
192
|
+
|
|
193
|
+
- **Models**: Pydantic v2 models (type-safe DTOs)
|
|
194
|
+
- **Repositories**: SQLAlchemy async for PostgreSQL (SELECT FOR UPDATE SKIP LOCKED)
|
|
195
|
+
- **Services**: `OutboxService`, `InboxService` with exponential backoff and event emission
|
|
196
|
+
- **Publishers**: `AioPikaPublisher` for RabbitMQ (connect_robust, auto-reconnect)
|
|
197
|
+
- **Consumers**: `AioPikaConsumer` with inbox recording and deduplication
|
|
198
|
+
- **Client**: `EventForgeAgentClient` fire-and-forget facade
|
|
199
|
+
|
|
200
|
+
## Features
|
|
201
|
+
|
|
202
|
+
- **Transactional Guarantees** — Messages created in same transaction as business logic
|
|
203
|
+
- **Automatic Retry** — Exponential backoff with jitter for failed publishes
|
|
204
|
+
- **Duplicate Detection** — Inbox pattern prevents duplicate processing
|
|
205
|
+
- **Async/Await** — Full async support with SQLAlchemy 2.0
|
|
206
|
+
- **Type Safety** — Pydantic v2 models for validation
|
|
207
|
+
- **Wire Format Compatible** — camelCase JSON body, AMQP headers match Node.js publisher
|
|
208
|
+
- **Auto-Reconnect** — aio-pika connect_robust for RabbitMQ resilience
|
|
209
|
+
- **Event Emission** — SimpleEventEmitter for lifecycle hooks
|
|
210
|
+
|
|
211
|
+
## Node.js Compatibility
|
|
212
|
+
|
|
213
|
+
For TypeScript/JavaScript projects, use the NPM packages:
|
|
214
|
+
- `@prodforcode/event-forge-core`
|
|
215
|
+
- `@prodforcode/event-forge-typeorm`
|
|
216
|
+
- `@prodforcode/event-forge-mongoose`
|
|
217
|
+
- `@prodforcode/event-forge-rabbitmq-publisher`
|
|
218
|
+
- `@prodforcode/event-forge-nestjs`
|
|
219
|
+
|
|
220
|
+
## Documentation
|
|
221
|
+
|
|
222
|
+
For full documentation, see the [main README](../../README_INBOX_OUTBOX.md).
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Event-Forge (Python)
|
|
2
|
+
|
|
3
|
+
Python implementation of the Universal Inbox-Outbox Pattern for reliable message delivery in distributed systems.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install event-forge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
With RabbitMQ support:
|
|
12
|
+
```bash
|
|
13
|
+
pip install event-forge[rabbitmq]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
With MongoDB support:
|
|
17
|
+
```bash
|
|
18
|
+
pip install event-forge[mongodb]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Python >=3.10
|
|
24
|
+
- SQLAlchemy >= 2.0 (async support)
|
|
25
|
+
- Pydantic >= 2.0
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### PostgreSQL + SQLAlchemy
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from event_forge import OutboxService, CreateOutboxMessageDto, OutboxConfig
|
|
33
|
+
from event_forge.repositories.sqlalchemy import (
|
|
34
|
+
SQLAlchemyOutboxRepository,
|
|
35
|
+
Base,
|
|
36
|
+
)
|
|
37
|
+
from event_forge.publishers.aio_pika_publisher import AioPikaPublisher
|
|
38
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
39
|
+
|
|
40
|
+
# Setup database
|
|
41
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
|
|
42
|
+
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
43
|
+
|
|
44
|
+
# Create tables
|
|
45
|
+
async with engine.begin() as conn:
|
|
46
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
47
|
+
|
|
48
|
+
# Setup repository and publisher
|
|
49
|
+
repository = SQLAlchemyOutboxRepository(session_factory)
|
|
50
|
+
publisher = AioPikaPublisher(
|
|
51
|
+
url="amqp://guest:guest@localhost/",
|
|
52
|
+
exchange_name="events",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Create service
|
|
56
|
+
outbox = OutboxService(
|
|
57
|
+
repository=repository,
|
|
58
|
+
publisher=publisher,
|
|
59
|
+
config=OutboxConfig(polling_interval=1.0, batch_size=10),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Connect publisher and start polling
|
|
63
|
+
await publisher.connect()
|
|
64
|
+
outbox.start_polling()
|
|
65
|
+
|
|
66
|
+
# Create message in transaction
|
|
67
|
+
async def create_user_event(session):
|
|
68
|
+
dto = CreateOutboxMessageDto(
|
|
69
|
+
aggregate_type="User",
|
|
70
|
+
aggregate_id="user-123",
|
|
71
|
+
event_type="user.created",
|
|
72
|
+
payload={"email": "user@example.com", "name": "John Doe"},
|
|
73
|
+
)
|
|
74
|
+
return await outbox.create_message(dto, session)
|
|
75
|
+
|
|
76
|
+
await outbox.with_transaction(create_user_event)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Fire-and-Forget Client
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from event_forge import EventForgeAgentClient, OutboxConfig
|
|
83
|
+
from event_forge.repositories.sqlalchemy import SQLAlchemyOutboxRepository
|
|
84
|
+
from event_forge.publishers.aio_pika_publisher import AioPikaPublisher
|
|
85
|
+
|
|
86
|
+
client = EventForgeAgentClient(
|
|
87
|
+
repository=SQLAlchemyOutboxRepository(session_factory),
|
|
88
|
+
publisher=AioPikaPublisher(url="amqp://localhost/", exchange_name="events"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
await client.start()
|
|
92
|
+
|
|
93
|
+
# Fire-and-forget
|
|
94
|
+
await client.fire(
|
|
95
|
+
aggregate_type="Call",
|
|
96
|
+
aggregate_id="call-123",
|
|
97
|
+
event_type="call.completed",
|
|
98
|
+
payload={"duration": 120, "status": "success"},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
await client.stop()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Inbox Pattern
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from event_forge import InboxService, CreateInboxMessageDto, InboxConfig
|
|
108
|
+
from event_forge.repositories.sqlalchemy import SQLAlchemyInboxRepository
|
|
109
|
+
|
|
110
|
+
# Setup
|
|
111
|
+
repository = SQLAlchemyInboxRepository(session_factory)
|
|
112
|
+
inbox = InboxService(
|
|
113
|
+
repository=repository,
|
|
114
|
+
config=InboxConfig(enable_retry=True),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Register handler
|
|
118
|
+
async def handle_order(message):
|
|
119
|
+
print(f"Processing order: {message.payload}")
|
|
120
|
+
|
|
121
|
+
inbox.register_handler("order.placed", handle_order)
|
|
122
|
+
|
|
123
|
+
# Process incoming message (with automatic deduplication)
|
|
124
|
+
dto = CreateInboxMessageDto(
|
|
125
|
+
message_id="ext-msg-123",
|
|
126
|
+
source="external-system",
|
|
127
|
+
event_type="order.placed",
|
|
128
|
+
payload={"order_id": "order-456", "amount": 99.99},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
await inbox.receive_message(dto)
|
|
132
|
+
|
|
133
|
+
# Start retry polling for failed messages
|
|
134
|
+
inbox.start_retry_polling()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### RabbitMQ Consumer
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from event_forge.consumers import AioPikaConsumer
|
|
141
|
+
|
|
142
|
+
consumer = AioPikaConsumer(
|
|
143
|
+
url="amqp://guest:guest@localhost/",
|
|
144
|
+
inbox_service=inbox,
|
|
145
|
+
queue_name="my-service-inbox",
|
|
146
|
+
exchange_name="events",
|
|
147
|
+
routing_keys=["Order.*", "User.*"],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
await consumer.start()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Architecture
|
|
154
|
+
|
|
155
|
+
Matches the TypeScript implementation with full wire format compatibility:
|
|
156
|
+
|
|
157
|
+
- **Models**: Pydantic v2 models (type-safe DTOs)
|
|
158
|
+
- **Repositories**: SQLAlchemy async for PostgreSQL (SELECT FOR UPDATE SKIP LOCKED)
|
|
159
|
+
- **Services**: `OutboxService`, `InboxService` with exponential backoff and event emission
|
|
160
|
+
- **Publishers**: `AioPikaPublisher` for RabbitMQ (connect_robust, auto-reconnect)
|
|
161
|
+
- **Consumers**: `AioPikaConsumer` with inbox recording and deduplication
|
|
162
|
+
- **Client**: `EventForgeAgentClient` fire-and-forget facade
|
|
163
|
+
|
|
164
|
+
## Features
|
|
165
|
+
|
|
166
|
+
- **Transactional Guarantees** — Messages created in same transaction as business logic
|
|
167
|
+
- **Automatic Retry** — Exponential backoff with jitter for failed publishes
|
|
168
|
+
- **Duplicate Detection** — Inbox pattern prevents duplicate processing
|
|
169
|
+
- **Async/Await** — Full async support with SQLAlchemy 2.0
|
|
170
|
+
- **Type Safety** — Pydantic v2 models for validation
|
|
171
|
+
- **Wire Format Compatible** — camelCase JSON body, AMQP headers match Node.js publisher
|
|
172
|
+
- **Auto-Reconnect** — aio-pika connect_robust for RabbitMQ resilience
|
|
173
|
+
- **Event Emission** — SimpleEventEmitter for lifecycle hooks
|
|
174
|
+
|
|
175
|
+
## Node.js Compatibility
|
|
176
|
+
|
|
177
|
+
For TypeScript/JavaScript projects, use the NPM packages:
|
|
178
|
+
- `@prodforcode/event-forge-core`
|
|
179
|
+
- `@prodforcode/event-forge-typeorm`
|
|
180
|
+
- `@prodforcode/event-forge-mongoose`
|
|
181
|
+
- `@prodforcode/event-forge-rabbitmq-publisher`
|
|
182
|
+
- `@prodforcode/event-forge-nestjs`
|
|
183
|
+
|
|
184
|
+
## Documentation
|
|
185
|
+
|
|
186
|
+
For full documentation, see the [main README](../../README_INBOX_OUTBOX.md).
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "event-forge"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "Universal Inbox-Outbox Pattern Library for Python - reliable message delivery in distributed systems"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "ProdForCode", email = "dev@prodforcode.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["inbox", "outbox", "transactional", "message", "pattern", "postgresql", "mongodb", "rabbitmq", "sqlalchemy"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Framework :: AsyncIO",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pydantic>=2.0.0,<3.0.0",
|
|
30
|
+
"sqlalchemy[asyncio]>=2.0.0,<3.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
rabbitmq = [
|
|
35
|
+
"aio-pika>=9.0.0,<10.0.0",
|
|
36
|
+
]
|
|
37
|
+
mongodb = [
|
|
38
|
+
"motor>=3.3.0,<4.0.0",
|
|
39
|
+
]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=7.4.0",
|
|
42
|
+
"pytest-asyncio>=0.21.0",
|
|
43
|
+
"pytest-cov>=4.1.0",
|
|
44
|
+
"black>=23.7.0",
|
|
45
|
+
"ruff>=0.0.285",
|
|
46
|
+
"mypy>=1.5.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://github.com/codeforprod/Event-Forge"
|
|
51
|
+
Documentation = "https://github.com/codeforprod/Event-Forge/tree/main/packages/python"
|
|
52
|
+
Repository = "https://github.com/codeforprod/Event-Forge"
|
|
53
|
+
Issues = "https://github.com/codeforprod/Event-Forge/issues"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/event_forge"]
|
|
57
|
+
|
|
58
|
+
[tool.black]
|
|
59
|
+
line-length = 100
|
|
60
|
+
target-version = ['py310', 'py311', 'py312']
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 100
|
|
64
|
+
target-version = "py310"
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
python_version = "3.10"
|
|
68
|
+
warn_return_any = true
|
|
69
|
+
warn_unused_configs = true
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
asyncio_mode = "auto"
|
|
74
|
+
addopts = "-v --tb=short"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Event-Forge Inbox-Outbox Pattern Library for Python."""
|
|
2
|
+
__version__ = "1.1.0"
|
|
3
|
+
|
|
4
|
+
# Models
|
|
5
|
+
from .models import (
|
|
6
|
+
CreateInboxMessageDto,
|
|
7
|
+
CreateOutboxMessageDto,
|
|
8
|
+
InboxMessage,
|
|
9
|
+
InboxMessageStatus,
|
|
10
|
+
OutboxMessage,
|
|
11
|
+
OutboxMessageStatus,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Errors
|
|
15
|
+
from .errors import DuplicateMessageError, ProcessingError
|
|
16
|
+
|
|
17
|
+
# Config
|
|
18
|
+
from .config import InboxConfig, OutboxConfig
|
|
19
|
+
|
|
20
|
+
# Events
|
|
21
|
+
from .events import InboxEvents, OutboxEvents, SimpleEventEmitter
|
|
22
|
+
|
|
23
|
+
# Services
|
|
24
|
+
from .services import InboxService, OutboxService
|
|
25
|
+
|
|
26
|
+
# Repository interfaces
|
|
27
|
+
from .repositories import IInboxRepository, IOutboxRepository, RecordInboxMessageResult
|
|
28
|
+
|
|
29
|
+
# Publisher interfaces
|
|
30
|
+
from .publishers import IMessagePublisher, PublishOptions
|
|
31
|
+
|
|
32
|
+
# Client facade
|
|
33
|
+
from .client import EventForgeAgentClient
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# Models
|
|
37
|
+
"CreateInboxMessageDto",
|
|
38
|
+
"CreateOutboxMessageDto",
|
|
39
|
+
"InboxMessage",
|
|
40
|
+
"InboxMessageStatus",
|
|
41
|
+
"OutboxMessage",
|
|
42
|
+
"OutboxMessageStatus",
|
|
43
|
+
# Errors
|
|
44
|
+
"DuplicateMessageError",
|
|
45
|
+
"ProcessingError",
|
|
46
|
+
# Config
|
|
47
|
+
"InboxConfig",
|
|
48
|
+
"OutboxConfig",
|
|
49
|
+
# Events
|
|
50
|
+
"InboxEvents",
|
|
51
|
+
"OutboxEvents",
|
|
52
|
+
"SimpleEventEmitter",
|
|
53
|
+
# Services
|
|
54
|
+
"InboxService",
|
|
55
|
+
"OutboxService",
|
|
56
|
+
# Repository interfaces
|
|
57
|
+
"IInboxRepository",
|
|
58
|
+
"IOutboxRepository",
|
|
59
|
+
"RecordInboxMessageResult",
|
|
60
|
+
# Publisher interfaces
|
|
61
|
+
"IMessagePublisher",
|
|
62
|
+
"PublishOptions",
|
|
63
|
+
# Client facade
|
|
64
|
+
"EventForgeAgentClient",
|
|
65
|
+
]
|