youeduc-sdk-messaging 0.4.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.
- youeduc_sdk_messaging-0.4.0/.gitignore +17 -0
- youeduc_sdk_messaging-0.4.0/LICENSE +21 -0
- youeduc_sdk_messaging-0.4.0/PKG-INFO +244 -0
- youeduc_sdk_messaging-0.4.0/README.md +205 -0
- youeduc_sdk_messaging-0.4.0/pyproject.toml +96 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/__init__.py +104 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/_schema_semantics.py +254 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/admin.py +161 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/config.py +86 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/consumer.py +1361 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/__init__.py +50 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/schemas/usuarios/usuario-atualizado/v1.json +343 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/schemas/usuarios/usuario-criado/v1.json +308 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/schemas/usuarios/usuario-desativado/v1.json +42 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/schemas/usuarios/usuario-login-alterado/v1.json +46 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/schemas/usuarios/usuario-mesclado/v1.json +46 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/schemas/usuarios/usuario-reativado/v1.json +42 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/schemas/usuarios/usuario-removido/v1.json +42 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/contracts/usuarios.py +308 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/dedup.py +65 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/dlq.py +257 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/exceptions.py +57 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/headers.py +152 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/integrations/__init__.py +1 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/integrations/django.py +269 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/integrations/flask.py +188 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/integrations/redis.py +268 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/integrations/runtime.py +1086 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/messaging.py +249 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/naming.py +169 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/publisher.py +190 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/py.typed +0 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/retry.py +39 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/schema.py +338 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/telemetry.py +310 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/transport.py +190 -0
- youeduc_sdk_messaging-0.4.0/src/sdk_messaging/types.py +172 -0
- youeduc_sdk_messaging-0.4.0/tests/__init__.py +0 -0
- youeduc_sdk_messaging-0.4.0/tests/integration/__init__.py +0 -0
- youeduc_sdk_messaging-0.4.0/tests/integration/conftest.py +448 -0
- youeduc_sdk_messaging-0.4.0/tests/integration/test_publish_consume.py +234 -0
- youeduc_sdk_messaging-0.4.0/tests/integration/test_resource_prefix.py +149 -0
- youeduc_sdk_messaging-0.4.0/tests/integration/test_retry_dlq.py +376 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/__init__.py +0 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/fakes_consumer.py +624 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/stubs_django.py +102 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/stubs_flask.py +73 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/stubs_integrations.py +159 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_admin.py +257 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_config.py +162 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_consumer_fixes.py +466 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_consumer_lifecycle.py +274 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_consumer_processing.py +504 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_consumer_regressions.py +523 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_contracts_usuarios.py +178 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_dedup.py +127 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_dlq.py +547 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_exceptions.py +55 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_generated_contracts.py +206 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_headers.py +277 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_integrations_django.py +340 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_integrations_flask.py +275 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_integrations_redis.py +448 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_integrations_runtime.py +1308 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_messaging.py +237 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_naming.py +144 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_packaging.py +26 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_protocol_conformance.py +70 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_publisher.py +484 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_resource_prefix.py +435 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_retry.py +69 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_schema.py +679 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_spec_vectors.py +1253 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_telemetry.py +509 -0
- youeduc_sdk_messaging-0.4.0/tests/unit/test_transport.py +343 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 YouEduc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: youeduc-sdk-messaging
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: SDK de mensageria assíncrona orientada a eventos sobre Apache Kafka (aiokafka)
|
|
5
|
+
Author: YouEduc
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: aiokafka,asyncio,dlq,events,json-schema,kafka,messaging,retry
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: aiokafka<0.15,>=0.14.0
|
|
24
|
+
Requires-Dist: jsonschema>=4.23.0
|
|
25
|
+
Requires-Dist: opentelemetry-api>=1.27.0
|
|
26
|
+
Requires-Dist: referencing>=0.28.4
|
|
27
|
+
Requires-Dist: structlog>=24.4.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy==2.3.1; extra == 'dev'
|
|
30
|
+
Requires-Dist: opentelemetry-sdk==1.44.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio==1.4.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest==9.1.1; extra == 'dev'
|
|
33
|
+
Requires-Dist: redis==8.1.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff==0.16.8; extra == 'dev'
|
|
35
|
+
Requires-Dist: types-jsonschema==4.26.0.20260518; extra == 'dev'
|
|
36
|
+
Provides-Extra: redis
|
|
37
|
+
Requires-Dist: redis>=5.0.1; extra == 'redis'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# youeduc-sdk-messaging
|
|
41
|
+
|
|
42
|
+
SDK Python de mensageria assíncrona orientada a eventos sobre **Apache Kafka**, construída sobre `aiokafka` e
|
|
43
|
+
`asyncio`. Publica eventos validados por JSON Schema e os consome com retry por tópicos, DLQ, deduplicação, commit
|
|
44
|
+
manual de offsets e observabilidade (OpenTelemetry + logs estruturados com `structlog`).
|
|
45
|
+
|
|
46
|
+
- Distribuição: `youeduc-sdk-messaging`; import: `sdk_messaging`.
|
|
47
|
+
- Tipada (`py.typed`), Python >= 3.10, Kafka >= 3.x.
|
|
48
|
+
- Existe uma SDK Node equivalente que segue o mesmo protocolo (tópicos, headers, retry/DLQ e telemetria): um serviço
|
|
49
|
+
Python publica e um serviço Node consome, e o contrário.
|
|
50
|
+
|
|
51
|
+
## Instalação
|
|
52
|
+
|
|
53
|
+
Fixe sempre uma versão exata:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install "youeduc-sdk-messaging==X.Y.Z"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Com a deduplicação compartilhada em Redis (`sdk_messaging.integrations.redis`):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install "youeduc-sdk-messaging[redis]==X.Y.Z"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Conferir a versão instalada:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -c "import sdk_messaging; print(sdk_messaging.__version__)"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Dependências de runtime: `aiokafka`, `jsonschema`, `referencing`, `structlog` e `opentelemetry-api`. O
|
|
72
|
+
`opentelemetry-sdk` e os exporters ficam a cargo da aplicação. Django, Flask, FastAPI e Celery **não** são
|
|
73
|
+
dependências: os adaptadores `sdk_messaging.integrations.django` e `sdk_messaging.integrations.flask` só importam o
|
|
74
|
+
framework quando o próprio serviço os importa.
|
|
75
|
+
|
|
76
|
+
## Publicar
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import asyncio
|
|
80
|
+
from pathlib import Path
|
|
81
|
+
|
|
82
|
+
from sdk_messaging import FileSystemSchemaLoader, KafkaConfig, Messaging
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def main() -> None:
|
|
86
|
+
async with Messaging(
|
|
87
|
+
KafkaConfig.from_env(), # KAFKA_BOOTSTRAP_SERVERS etc.
|
|
88
|
+
service="enrollment-api", # kebab-case; vai no header `producer`
|
|
89
|
+
environment="production",
|
|
90
|
+
schema_loader=FileSystemSchemaLoader(Path("schemas")),
|
|
91
|
+
) as messaging:
|
|
92
|
+
result = await messaging.publish(
|
|
93
|
+
"enrollment", "student", "enrolled", "v1", # domain, aggregate, event, version
|
|
94
|
+
{"student_id": "s-123", "course_id": "c-9"},
|
|
95
|
+
key="s-123", # define a partição (ordem por chave)
|
|
96
|
+
idempotency_key="enrollment-987", # opcional; chave de dedup preferida
|
|
97
|
+
)
|
|
98
|
+
print(result.message_id, result.topic, result.partition, result.offset)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
O evento vai para o tópico `enrollment.events.v1` com `event_type = "student.enrolled"` e é validado contra
|
|
105
|
+
`schemas/enrollment/student-enrolled/v1.json` (JSON Schema Draft 2020-12) antes de sair. `publish` aguarda o ack do
|
|
106
|
+
broker; payload inválido levanta `SchemaValidationError` sem enviar nada, e erro do Kafka levanta `PublishError`.
|
|
107
|
+
|
|
108
|
+
## Consumir
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import asyncio
|
|
112
|
+
import signal
|
|
113
|
+
from pathlib import Path
|
|
114
|
+
|
|
115
|
+
from sdk_messaging import (
|
|
116
|
+
ConsumedMessage,
|
|
117
|
+
FileSystemSchemaLoader,
|
|
118
|
+
KafkaConfig,
|
|
119
|
+
Messaging,
|
|
120
|
+
PermanentProcessingError,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def on_enrolled(message: ConsumedMessage) -> None:
|
|
125
|
+
if "course_id" not in message.payload:
|
|
126
|
+
raise PermanentProcessingError("course_id ausente") # vai direto para a DLQ
|
|
127
|
+
... # retornar = sucesso; qualquer outra exceção = retry
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def main() -> None:
|
|
131
|
+
stop = asyncio.Event()
|
|
132
|
+
loop = asyncio.get_running_loop()
|
|
133
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
134
|
+
loop.add_signal_handler(sig, stop.set)
|
|
135
|
+
|
|
136
|
+
async with Messaging(
|
|
137
|
+
KafkaConfig.from_env(),
|
|
138
|
+
service="learning-worker", # também é o consumer group padrão
|
|
139
|
+
environment="production",
|
|
140
|
+
schema_loader=FileSystemSchemaLoader(Path("schemas")),
|
|
141
|
+
) as messaging:
|
|
142
|
+
consumer = messaging.consumer(
|
|
143
|
+
domain="enrollment",
|
|
144
|
+
version="v1",
|
|
145
|
+
handlers={"student.enrolled": on_enrolled}, # event_type -> handler async
|
|
146
|
+
processing_timeout_s=60,
|
|
147
|
+
)
|
|
148
|
+
await consumer.start()
|
|
149
|
+
consumer_done = asyncio.ensure_future(consumer.wait())
|
|
150
|
+
signal_received = asyncio.ensure_future(stop.wait())
|
|
151
|
+
await asyncio.wait({consumer_done, signal_received}, return_when=asyncio.FIRST_COMPLETED)
|
|
152
|
+
signal_received.cancel()
|
|
153
|
+
# Ao sair do bloco, os consumers terminam o registro em voo, commitam e o producer fecha.
|
|
154
|
+
await consumer_done # relança o erro fatal que derrubou o consumer, se houve
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
asyncio.run(main())
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Eventos do domínio sem handler são ignorados e o offset avança. Os tópicos (principal, retry e DLQ do grupo) precisam
|
|
161
|
+
existir antes do consumer iniciar: crie-os por IaC ou, em desenvolvimento, com `messaging.ensure_topics(...)`.
|
|
162
|
+
|
|
163
|
+
## Garantias
|
|
164
|
+
|
|
165
|
+
| Garantia | Como |
|
|
166
|
+
|---|---|
|
|
167
|
+
| **At-least-once** no consumo | O offset só é commitado depois do desfecho final (sucesso, DLQ, ignorada) ou do envio **confirmado** para retry/DLQ. Um handler pode rodar mais de uma vez para a mesma mensagem: **handlers devem ser idempotentes** ou usar um `DuplicateChecker`. |
|
|
168
|
+
| **Ordem por chave** | Mensagens com a mesma `key` vão para a mesma partição e são processadas em sequência. Uma mensagem que vai para retry sai da ordem. |
|
|
169
|
+
| **Producer idempotente** | Um producer por processo, `acks=all` e idempotência ligada, compartilhado por publish, retry e DLQ. |
|
|
170
|
+
| **Schema obrigatório** | Todo payload é validado no publish e no consumo. |
|
|
171
|
+
| **Sem PII em logs** | Logs, spans, métricas e erros de schema nunca incluem valores do payload nem a `key`. |
|
|
172
|
+
|
|
173
|
+
Não há exactly-once nem transações Kafka.
|
|
174
|
+
|
|
175
|
+
## Retry e DLQ
|
|
176
|
+
|
|
177
|
+
A política padrão (`RetryPolicy()`) reprocessa uma falha transitória após **30 s, 5 min, 1 h e 12 h**; a quinta falha
|
|
178
|
+
vai para a DLQ. Cada atraso tem um tópico de retry por consumer group (`{tópico}.{grupo}.retry.30s` ... `.retry.12h`)
|
|
179
|
+
e a DLQ é `{tópico}.{grupo}.dlq`. A mensagem de retry é relida só pelo próprio grupo e nunca volta ao tópico
|
|
180
|
+
principal; enquanto espera, a partição de retry fica pausada sem bloquear as demais.
|
|
181
|
+
|
|
182
|
+
- `PermanentProcessingError` (ou subclasse), payload inválido e falha de schema vão direto para a DLQ.
|
|
183
|
+
- Qualquer outra exceção, inclusive timeout de processamento, segue a política de retry.
|
|
184
|
+
- Política própria: `RetryPolicy(delays_s=(10, 60, 300))`; política vazia desativa o retry.
|
|
185
|
+
- A DLQ nunca marca a deduplicação, então `replay_dlq(...)` pode reprocessar as mensagens.
|
|
186
|
+
|
|
187
|
+
## Prefixo de ambiente
|
|
188
|
+
|
|
189
|
+
Para homologação e produção no mesmo cluster, `resource_prefix` (kebab-case, como `hmg` ou `prd`) vira o primeiro
|
|
190
|
+
segmento do tópico principal e do consumer group, e por consequência de retry, DLQ e replay:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
Messaging(kafka, service="catalogo-api", environment="homologacao", schema_loader=loader, resource_prefix="hmg")
|
|
194
|
+
# tópico: hmg.usuarios.events.v1 grupo: hmg.catalogo-api
|
|
195
|
+
# retry: hmg.usuarios.events.v1.catalogo-api.retry.30s DLQ: hmg.usuarios.events.v1.catalogo-api.dlq
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Prefixar também o grupo impede que réplicas dos dois ambientes dividam as partições do mesmo `group.id`. Sem prefixo,
|
|
199
|
+
os nomes não mudam.
|
|
200
|
+
|
|
201
|
+
## Deduplicação
|
|
202
|
+
|
|
203
|
+
`InMemoryDuplicateChecker` serve a um processo só. Para várias réplicas, use o store Redis (extra `[redis]`), com um
|
|
204
|
+
`namespace` por sistema e ambiente:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from sdk_messaging.integrations.redis import RedisDedupStore
|
|
208
|
+
|
|
209
|
+
store = RedisDedupStore.from_url("redis://redis:6379/0", namespace="hmgcatalogo_api")
|
|
210
|
+
consumer = messaging.consumer(
|
|
211
|
+
domain="enrollment",
|
|
212
|
+
version="v1",
|
|
213
|
+
handlers={"student.enrolled": on_enrolled},
|
|
214
|
+
duplicate_checker=store.checker("learning-worker"),
|
|
215
|
+
)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
A chave é marcada só depois do sucesso do handler; retry e DLQ não marcam.
|
|
219
|
+
|
|
220
|
+
## Contratos embutidos
|
|
221
|
+
|
|
222
|
+
Os schemas e tipos (`TypedDict`) dos eventos corporativos vêm no pacote, na mesma versão da SDK:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from sdk_messaging.contracts import schema_loader
|
|
226
|
+
from sdk_messaging.contracts.usuarios import UsuarioDesativado
|
|
227
|
+
|
|
228
|
+
loader = schema_loader() # InMemorySchemaLoader com todos os schemas embutidos
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Para combinar com schemas próprios do serviço:
|
|
232
|
+
`ChainSchemaLoader(FileSystemSchemaLoader(Path("schemas")), schema_loader())`.
|
|
233
|
+
|
|
234
|
+
## Configuração
|
|
235
|
+
|
|
236
|
+
`KafkaConfig.from_env()` lê `KAFKA_BOOTSTRAP_SERVERS` (obrigatória), `KAFKA_CLIENT_ID`, `KAFKA_SECURITY_PROTOCOL`
|
|
237
|
+
(`PLAINTEXT`, `SSL`, `SASL_PLAINTEXT`, `SASL_SSL`), `KAFKA_SASL_MECHANISM`, `KAFKA_SASL_USERNAME` e
|
|
238
|
+
`KAFKA_SASL_PASSWORD`. Django, Flask e serviços sem framework também podem montar tudo a partir de um mapa
|
|
239
|
+
`SDK_MESSAGING` (`SERVICE`, `ENVIRONMENT`, `KAFKA`, `REDIS`, `RESOURCE_PREFIX`...) com
|
|
240
|
+
`sdk_messaging.integrations.runtime.MessagingSettings.from_mapping`.
|
|
241
|
+
|
|
242
|
+
## Licença
|
|
243
|
+
|
|
244
|
+
MIT.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# youeduc-sdk-messaging
|
|
2
|
+
|
|
3
|
+
SDK Python de mensageria assíncrona orientada a eventos sobre **Apache Kafka**, construída sobre `aiokafka` e
|
|
4
|
+
`asyncio`. Publica eventos validados por JSON Schema e os consome com retry por tópicos, DLQ, deduplicação, commit
|
|
5
|
+
manual de offsets e observabilidade (OpenTelemetry + logs estruturados com `structlog`).
|
|
6
|
+
|
|
7
|
+
- Distribuição: `youeduc-sdk-messaging`; import: `sdk_messaging`.
|
|
8
|
+
- Tipada (`py.typed`), Python >= 3.10, Kafka >= 3.x.
|
|
9
|
+
- Existe uma SDK Node equivalente que segue o mesmo protocolo (tópicos, headers, retry/DLQ e telemetria): um serviço
|
|
10
|
+
Python publica e um serviço Node consome, e o contrário.
|
|
11
|
+
|
|
12
|
+
## Instalação
|
|
13
|
+
|
|
14
|
+
Fixe sempre uma versão exata:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install "youeduc-sdk-messaging==X.Y.Z"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Com a deduplicação compartilhada em Redis (`sdk_messaging.integrations.redis`):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install "youeduc-sdk-messaging[redis]==X.Y.Z"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Conferir a versão instalada:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python -c "import sdk_messaging; print(sdk_messaging.__version__)"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Dependências de runtime: `aiokafka`, `jsonschema`, `referencing`, `structlog` e `opentelemetry-api`. O
|
|
33
|
+
`opentelemetry-sdk` e os exporters ficam a cargo da aplicação. Django, Flask, FastAPI e Celery **não** são
|
|
34
|
+
dependências: os adaptadores `sdk_messaging.integrations.django` e `sdk_messaging.integrations.flask` só importam o
|
|
35
|
+
framework quando o próprio serviço os importa.
|
|
36
|
+
|
|
37
|
+
## Publicar
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import asyncio
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
from sdk_messaging import FileSystemSchemaLoader, KafkaConfig, Messaging
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def main() -> None:
|
|
47
|
+
async with Messaging(
|
|
48
|
+
KafkaConfig.from_env(), # KAFKA_BOOTSTRAP_SERVERS etc.
|
|
49
|
+
service="enrollment-api", # kebab-case; vai no header `producer`
|
|
50
|
+
environment="production",
|
|
51
|
+
schema_loader=FileSystemSchemaLoader(Path("schemas")),
|
|
52
|
+
) as messaging:
|
|
53
|
+
result = await messaging.publish(
|
|
54
|
+
"enrollment", "student", "enrolled", "v1", # domain, aggregate, event, version
|
|
55
|
+
{"student_id": "s-123", "course_id": "c-9"},
|
|
56
|
+
key="s-123", # define a partição (ordem por chave)
|
|
57
|
+
idempotency_key="enrollment-987", # opcional; chave de dedup preferida
|
|
58
|
+
)
|
|
59
|
+
print(result.message_id, result.topic, result.partition, result.offset)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
asyncio.run(main())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
O evento vai para o tópico `enrollment.events.v1` com `event_type = "student.enrolled"` e é validado contra
|
|
66
|
+
`schemas/enrollment/student-enrolled/v1.json` (JSON Schema Draft 2020-12) antes de sair. `publish` aguarda o ack do
|
|
67
|
+
broker; payload inválido levanta `SchemaValidationError` sem enviar nada, e erro do Kafka levanta `PublishError`.
|
|
68
|
+
|
|
69
|
+
## Consumir
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import asyncio
|
|
73
|
+
import signal
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
|
|
76
|
+
from sdk_messaging import (
|
|
77
|
+
ConsumedMessage,
|
|
78
|
+
FileSystemSchemaLoader,
|
|
79
|
+
KafkaConfig,
|
|
80
|
+
Messaging,
|
|
81
|
+
PermanentProcessingError,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def on_enrolled(message: ConsumedMessage) -> None:
|
|
86
|
+
if "course_id" not in message.payload:
|
|
87
|
+
raise PermanentProcessingError("course_id ausente") # vai direto para a DLQ
|
|
88
|
+
... # retornar = sucesso; qualquer outra exceção = retry
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def main() -> None:
|
|
92
|
+
stop = asyncio.Event()
|
|
93
|
+
loop = asyncio.get_running_loop()
|
|
94
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
95
|
+
loop.add_signal_handler(sig, stop.set)
|
|
96
|
+
|
|
97
|
+
async with Messaging(
|
|
98
|
+
KafkaConfig.from_env(),
|
|
99
|
+
service="learning-worker", # também é o consumer group padrão
|
|
100
|
+
environment="production",
|
|
101
|
+
schema_loader=FileSystemSchemaLoader(Path("schemas")),
|
|
102
|
+
) as messaging:
|
|
103
|
+
consumer = messaging.consumer(
|
|
104
|
+
domain="enrollment",
|
|
105
|
+
version="v1",
|
|
106
|
+
handlers={"student.enrolled": on_enrolled}, # event_type -> handler async
|
|
107
|
+
processing_timeout_s=60,
|
|
108
|
+
)
|
|
109
|
+
await consumer.start()
|
|
110
|
+
consumer_done = asyncio.ensure_future(consumer.wait())
|
|
111
|
+
signal_received = asyncio.ensure_future(stop.wait())
|
|
112
|
+
await asyncio.wait({consumer_done, signal_received}, return_when=asyncio.FIRST_COMPLETED)
|
|
113
|
+
signal_received.cancel()
|
|
114
|
+
# Ao sair do bloco, os consumers terminam o registro em voo, commitam e o producer fecha.
|
|
115
|
+
await consumer_done # relança o erro fatal que derrubou o consumer, se houve
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
asyncio.run(main())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Eventos do domínio sem handler são ignorados e o offset avança. Os tópicos (principal, retry e DLQ do grupo) precisam
|
|
122
|
+
existir antes do consumer iniciar: crie-os por IaC ou, em desenvolvimento, com `messaging.ensure_topics(...)`.
|
|
123
|
+
|
|
124
|
+
## Garantias
|
|
125
|
+
|
|
126
|
+
| Garantia | Como |
|
|
127
|
+
|---|---|
|
|
128
|
+
| **At-least-once** no consumo | O offset só é commitado depois do desfecho final (sucesso, DLQ, ignorada) ou do envio **confirmado** para retry/DLQ. Um handler pode rodar mais de uma vez para a mesma mensagem: **handlers devem ser idempotentes** ou usar um `DuplicateChecker`. |
|
|
129
|
+
| **Ordem por chave** | Mensagens com a mesma `key` vão para a mesma partição e são processadas em sequência. Uma mensagem que vai para retry sai da ordem. |
|
|
130
|
+
| **Producer idempotente** | Um producer por processo, `acks=all` e idempotência ligada, compartilhado por publish, retry e DLQ. |
|
|
131
|
+
| **Schema obrigatório** | Todo payload é validado no publish e no consumo. |
|
|
132
|
+
| **Sem PII em logs** | Logs, spans, métricas e erros de schema nunca incluem valores do payload nem a `key`. |
|
|
133
|
+
|
|
134
|
+
Não há exactly-once nem transações Kafka.
|
|
135
|
+
|
|
136
|
+
## Retry e DLQ
|
|
137
|
+
|
|
138
|
+
A política padrão (`RetryPolicy()`) reprocessa uma falha transitória após **30 s, 5 min, 1 h e 12 h**; a quinta falha
|
|
139
|
+
vai para a DLQ. Cada atraso tem um tópico de retry por consumer group (`{tópico}.{grupo}.retry.30s` ... `.retry.12h`)
|
|
140
|
+
e a DLQ é `{tópico}.{grupo}.dlq`. A mensagem de retry é relida só pelo próprio grupo e nunca volta ao tópico
|
|
141
|
+
principal; enquanto espera, a partição de retry fica pausada sem bloquear as demais.
|
|
142
|
+
|
|
143
|
+
- `PermanentProcessingError` (ou subclasse), payload inválido e falha de schema vão direto para a DLQ.
|
|
144
|
+
- Qualquer outra exceção, inclusive timeout de processamento, segue a política de retry.
|
|
145
|
+
- Política própria: `RetryPolicy(delays_s=(10, 60, 300))`; política vazia desativa o retry.
|
|
146
|
+
- A DLQ nunca marca a deduplicação, então `replay_dlq(...)` pode reprocessar as mensagens.
|
|
147
|
+
|
|
148
|
+
## Prefixo de ambiente
|
|
149
|
+
|
|
150
|
+
Para homologação e produção no mesmo cluster, `resource_prefix` (kebab-case, como `hmg` ou `prd`) vira o primeiro
|
|
151
|
+
segmento do tópico principal e do consumer group, e por consequência de retry, DLQ e replay:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
Messaging(kafka, service="catalogo-api", environment="homologacao", schema_loader=loader, resource_prefix="hmg")
|
|
155
|
+
# tópico: hmg.usuarios.events.v1 grupo: hmg.catalogo-api
|
|
156
|
+
# retry: hmg.usuarios.events.v1.catalogo-api.retry.30s DLQ: hmg.usuarios.events.v1.catalogo-api.dlq
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Prefixar também o grupo impede que réplicas dos dois ambientes dividam as partições do mesmo `group.id`. Sem prefixo,
|
|
160
|
+
os nomes não mudam.
|
|
161
|
+
|
|
162
|
+
## Deduplicação
|
|
163
|
+
|
|
164
|
+
`InMemoryDuplicateChecker` serve a um processo só. Para várias réplicas, use o store Redis (extra `[redis]`), com um
|
|
165
|
+
`namespace` por sistema e ambiente:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from sdk_messaging.integrations.redis import RedisDedupStore
|
|
169
|
+
|
|
170
|
+
store = RedisDedupStore.from_url("redis://redis:6379/0", namespace="hmgcatalogo_api")
|
|
171
|
+
consumer = messaging.consumer(
|
|
172
|
+
domain="enrollment",
|
|
173
|
+
version="v1",
|
|
174
|
+
handlers={"student.enrolled": on_enrolled},
|
|
175
|
+
duplicate_checker=store.checker("learning-worker"),
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A chave é marcada só depois do sucesso do handler; retry e DLQ não marcam.
|
|
180
|
+
|
|
181
|
+
## Contratos embutidos
|
|
182
|
+
|
|
183
|
+
Os schemas e tipos (`TypedDict`) dos eventos corporativos vêm no pacote, na mesma versão da SDK:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from sdk_messaging.contracts import schema_loader
|
|
187
|
+
from sdk_messaging.contracts.usuarios import UsuarioDesativado
|
|
188
|
+
|
|
189
|
+
loader = schema_loader() # InMemorySchemaLoader com todos os schemas embutidos
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Para combinar com schemas próprios do serviço:
|
|
193
|
+
`ChainSchemaLoader(FileSystemSchemaLoader(Path("schemas")), schema_loader())`.
|
|
194
|
+
|
|
195
|
+
## Configuração
|
|
196
|
+
|
|
197
|
+
`KafkaConfig.from_env()` lê `KAFKA_BOOTSTRAP_SERVERS` (obrigatória), `KAFKA_CLIENT_ID`, `KAFKA_SECURITY_PROTOCOL`
|
|
198
|
+
(`PLAINTEXT`, `SSL`, `SASL_PLAINTEXT`, `SASL_SSL`), `KAFKA_SASL_MECHANISM`, `KAFKA_SASL_USERNAME` e
|
|
199
|
+
`KAFKA_SASL_PASSWORD`. Django, Flask e serviços sem framework também podem montar tudo a partir de um mapa
|
|
200
|
+
`SDK_MESSAGING` (`SERVICE`, `ENVIRONMENT`, `KAFKA`, `REDIS`, `RESOURCE_PREFIX`...) com
|
|
201
|
+
`sdk_messaging.integrations.runtime.MessagingSettings.from_mapping`.
|
|
202
|
+
|
|
203
|
+
## Licença
|
|
204
|
+
|
|
205
|
+
MIT.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27", "hatch-vcs>=0.5.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "youeduc-sdk-messaging"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "SDK de mensageria assíncrona orientada a eventos sobre Apache Kafka (aiokafka)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
# PEP 639: expressão SPDX + arquivo; sem classifier "License ::" (redundante e rejeitado junto da expressão).
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
authors = [{ name = "YouEduc" }]
|
|
15
|
+
keywords = ["kafka", "aiokafka", "messaging", "events", "asyncio", "json-schema", "retry", "dlq"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Framework :: AsyncIO",
|
|
27
|
+
"Topic :: System :: Distributed Computing",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"aiokafka>=0.14.0,<0.15",
|
|
33
|
+
"jsonschema>=4.23.0",
|
|
34
|
+
"referencing>=0.28.4",
|
|
35
|
+
"structlog>=24.4.0",
|
|
36
|
+
"opentelemetry-api>=1.27.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
redis = ["redis>=5.0.1"]
|
|
41
|
+
dev = [
|
|
42
|
+
"redis==8.1.0",
|
|
43
|
+
"mypy==2.3.1",
|
|
44
|
+
"ruff==0.16.8",
|
|
45
|
+
"pytest==9.1.1",
|
|
46
|
+
"pytest-asyncio==1.4.0",
|
|
47
|
+
"opentelemetry-sdk==1.44.0",
|
|
48
|
+
"types-jsonschema==4.26.0.20260518",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Versão = tag git (vX.Y.Z, criada pelo auto-tag); sem git/tag, fallback 0.0.0.
|
|
52
|
+
[tool.hatch.version]
|
|
53
|
+
source = "vcs"
|
|
54
|
+
# O repositório git (e as tags vX.Y.Z, únicas para Python e Node) fica um nível acima.
|
|
55
|
+
raw-options = { root = "..", fallback_version = "0.0.0" }
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/sdk_messaging"]
|
|
59
|
+
|
|
60
|
+
# O sdist vai para o PyPI público: sem as instruções de agentes de IA do repositório.
|
|
61
|
+
[tool.hatch.build.targets.sdist]
|
|
62
|
+
exclude = ["CLAUDE.md"]
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
strict = true
|
|
66
|
+
python_version = "3.10"
|
|
67
|
+
mypy_path = "src"
|
|
68
|
+
files = ["src", "tests"]
|
|
69
|
+
|
|
70
|
+
[[tool.mypy.overrides]]
|
|
71
|
+
module = ["aiokafka", "aiokafka.*"]
|
|
72
|
+
ignore_missing_imports = true
|
|
73
|
+
|
|
74
|
+
[[tool.mypy.overrides]]
|
|
75
|
+
module = ["tests.*"]
|
|
76
|
+
disallow_untyped_defs = false
|
|
77
|
+
disallow_incomplete_defs = false
|
|
78
|
+
|
|
79
|
+
[tool.ruff]
|
|
80
|
+
target-version = "py310"
|
|
81
|
+
src = ["src", "tests"]
|
|
82
|
+
extend-exclude = ["*.md"]
|
|
83
|
+
|
|
84
|
+
[tool.ruff.lint]
|
|
85
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "TC", "ANN", "ASYNC", "RUF"]
|
|
86
|
+
ignore = ["E501"]
|
|
87
|
+
|
|
88
|
+
[tool.ruff.lint.per-file-ignores]
|
|
89
|
+
"tests/**" = ["ANN"]
|
|
90
|
+
|
|
91
|
+
[tool.pytest.ini_options]
|
|
92
|
+
asyncio_mode = "auto"
|
|
93
|
+
testpaths = ["tests"]
|
|
94
|
+
pythonpath = ["src"]
|
|
95
|
+
markers = ["integration: requer um broker Kafka real (KAFKA_BOOTSTRAP_SERVERS)"]
|
|
96
|
+
addopts = "-m 'not integration'"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""SDK de mensageria assíncrona sobre Apache Kafka."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _version
|
|
5
|
+
|
|
6
|
+
from sdk_messaging import naming
|
|
7
|
+
from sdk_messaging.admin import ensure_topics
|
|
8
|
+
from sdk_messaging.config import KafkaConfig
|
|
9
|
+
from sdk_messaging.consumer import (
|
|
10
|
+
Consumer,
|
|
11
|
+
HandlerCancelledError,
|
|
12
|
+
InvalidHandlerError,
|
|
13
|
+
InvalidPayloadError,
|
|
14
|
+
)
|
|
15
|
+
from sdk_messaging.dedup import InMemoryDuplicateChecker
|
|
16
|
+
from sdk_messaging.dlq import ReplayReport, replay_dlq
|
|
17
|
+
from sdk_messaging.exceptions import (
|
|
18
|
+
MessagingError,
|
|
19
|
+
NotStartedError,
|
|
20
|
+
PermanentProcessingError,
|
|
21
|
+
PublishError,
|
|
22
|
+
SchemaNotFoundError,
|
|
23
|
+
SchemaValidationError,
|
|
24
|
+
)
|
|
25
|
+
from sdk_messaging.messaging import Messaging
|
|
26
|
+
from sdk_messaging.publisher import Publisher
|
|
27
|
+
from sdk_messaging.retry import RetryPolicy
|
|
28
|
+
from sdk_messaging.schema import (
|
|
29
|
+
ChainSchemaLoader,
|
|
30
|
+
FileSystemSchemaLoader,
|
|
31
|
+
InMemorySchemaLoader,
|
|
32
|
+
JsonSchemaValidator,
|
|
33
|
+
SchemaRegistry,
|
|
34
|
+
)
|
|
35
|
+
from sdk_messaging.telemetry import NoopTelemetry, OtelTelemetry
|
|
36
|
+
from sdk_messaging.transport import KafkaProducerSender
|
|
37
|
+
from sdk_messaging.types import (
|
|
38
|
+
ConsumedMessage,
|
|
39
|
+
ConsumeOutcome,
|
|
40
|
+
ConsumeSpan,
|
|
41
|
+
DlqReason,
|
|
42
|
+
DuplicateChecker,
|
|
43
|
+
MessageHandler,
|
|
44
|
+
MessageInfo,
|
|
45
|
+
MessageSender,
|
|
46
|
+
PayloadValidator,
|
|
47
|
+
PublishResult,
|
|
48
|
+
PublishStatus,
|
|
49
|
+
SchemaLoader,
|
|
50
|
+
SchemaValidator,
|
|
51
|
+
SentRecord,
|
|
52
|
+
Telemetry,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
__version__ = _version("youeduc-sdk-messaging")
|
|
57
|
+
except PackageNotFoundError: # rodando do código-fonte, sem instalar
|
|
58
|
+
__version__ = "0.0.0"
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"ChainSchemaLoader",
|
|
62
|
+
"ConsumeOutcome",
|
|
63
|
+
"ConsumeSpan",
|
|
64
|
+
"ConsumedMessage",
|
|
65
|
+
"Consumer",
|
|
66
|
+
"DlqReason",
|
|
67
|
+
"DuplicateChecker",
|
|
68
|
+
"FileSystemSchemaLoader",
|
|
69
|
+
"HandlerCancelledError",
|
|
70
|
+
"InMemoryDuplicateChecker",
|
|
71
|
+
"InMemorySchemaLoader",
|
|
72
|
+
"InvalidHandlerError",
|
|
73
|
+
"InvalidPayloadError",
|
|
74
|
+
"JsonSchemaValidator",
|
|
75
|
+
"KafkaConfig",
|
|
76
|
+
"KafkaProducerSender",
|
|
77
|
+
"MessageHandler",
|
|
78
|
+
"MessageInfo",
|
|
79
|
+
"MessageSender",
|
|
80
|
+
"Messaging",
|
|
81
|
+
"MessagingError",
|
|
82
|
+
"NoopTelemetry",
|
|
83
|
+
"NotStartedError",
|
|
84
|
+
"OtelTelemetry",
|
|
85
|
+
"PayloadValidator",
|
|
86
|
+
"PermanentProcessingError",
|
|
87
|
+
"PublishError",
|
|
88
|
+
"PublishResult",
|
|
89
|
+
"PublishStatus",
|
|
90
|
+
"Publisher",
|
|
91
|
+
"ReplayReport",
|
|
92
|
+
"RetryPolicy",
|
|
93
|
+
"SchemaLoader",
|
|
94
|
+
"SchemaNotFoundError",
|
|
95
|
+
"SchemaRegistry",
|
|
96
|
+
"SchemaValidationError",
|
|
97
|
+
"SchemaValidator",
|
|
98
|
+
"SentRecord",
|
|
99
|
+
"Telemetry",
|
|
100
|
+
"__version__",
|
|
101
|
+
"ensure_topics",
|
|
102
|
+
"naming",
|
|
103
|
+
"replay_dlq",
|
|
104
|
+
]
|