streamgate 0.4.0__tar.gz → 1.0.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.
Files changed (107) hide show
  1. {streamgate-0.4.0 → streamgate-1.0.0}/.github/workflows/ci.yml +4 -4
  2. streamgate-1.0.0/CHANGELOG.md +499 -0
  3. streamgate-1.0.0/CONFIGURATION.md +186 -0
  4. streamgate-1.0.0/PKG-INFO +249 -0
  5. streamgate-1.0.0/README.md +213 -0
  6. streamgate-1.0.0/README.zh-CN.md +351 -0
  7. streamgate-1.0.0/examples/http_probe/consume.py +43 -0
  8. {streamgate-0.4.0 → streamgate-1.0.0}/examples/http_probe/health_server.py +4 -4
  9. {streamgate-0.4.0 → streamgate-1.0.0}/examples/http_probe/models.py +1 -1
  10. {streamgate-0.4.0 → streamgate-1.0.0}/examples/http_probe/produce.py +15 -34
  11. {streamgate-0.4.0 → streamgate-1.0.0}/examples/prod_pipeline/README.md +19 -18
  12. streamgate-1.0.0/examples/prod_pipeline/consume.py +59 -0
  13. {streamgate-0.4.0 → streamgate-1.0.0}/examples/prod_pipeline/health_server.py +4 -4
  14. {streamgate-0.4.0 → streamgate-1.0.0}/examples/prod_pipeline/models.py +6 -6
  15. streamgate-1.0.0/examples/prod_pipeline/produce.py +87 -0
  16. streamgate-1.0.0/examples/pure_pipeline/README.md +62 -0
  17. streamgate-1.0.0/examples/pure_pipeline/consume.py +76 -0
  18. {streamgate-0.4.0 → streamgate-1.0.0}/examples/pure_pipeline/models.py +1 -1
  19. streamgate-1.0.0/examples/pure_pipeline/produce.py +51 -0
  20. streamgate-1.0.0/examples/redis_admission/README.md +51 -0
  21. {streamgate-0.4.0 → streamgate-1.0.0}/examples/redis_admission/models.py +1 -1
  22. streamgate-1.0.0/examples/redis_admission/produce.py +68 -0
  23. streamgate-1.0.0/examples/sqlite_upsert/README.md +62 -0
  24. streamgate-1.0.0/examples/sqlite_upsert/consume.py +33 -0
  25. streamgate-1.0.0/examples/sqlite_upsert/models.py +22 -0
  26. streamgate-1.0.0/examples/sqlite_upsert/produce.py +36 -0
  27. {streamgate-0.4.0 → streamgate-1.0.0}/pyproject.toml +3 -3
  28. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/__init__.py +50 -46
  29. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/config.py +6 -26
  30. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/consumer/classifier.py +6 -6
  31. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/consumer/dlq.py +91 -94
  32. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/consumer/loop.py +206 -233
  33. streamgate-1.0.0/src/streamgate/consumer/options.py +261 -0
  34. streamgate-1.0.0/src/streamgate/consumer/runner.py +309 -0
  35. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/contrib/__init__.py +2 -2
  36. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/contrib/http_probe/probe_signal.py +1 -1
  37. streamgate-1.0.0/src/streamgate/contrib/mssql_upsert/__init__.py +73 -0
  38. streamgate-1.0.0/src/streamgate/contrib/redis_dedup/__init__.py +35 -0
  39. streamgate-1.0.0/src/streamgate/contrib/redis_dedup/cache.py +182 -0
  40. streamgate-1.0.0/src/streamgate/contrib/redis_dedup/carrier.py +379 -0
  41. streamgate-1.0.0/src/streamgate/contrib/redis_dedup/config.py +14 -0
  42. streamgate-1.0.0/src/streamgate/contrib/sql_upsert/__init__.py +37 -0
  43. {streamgate-0.4.0/src/streamgate/contrib/sql_sink → streamgate-1.0.0/src/streamgate/contrib/sql_upsert}/_dialects/__init__.py +1 -1
  44. {streamgate-0.4.0/src/streamgate/contrib/sql_sink → streamgate-1.0.0/src/streamgate/contrib/sql_upsert}/backfill.py +29 -29
  45. {streamgate-0.4.0/src/streamgate/contrib/sql_sink → streamgate-1.0.0/src/streamgate/contrib/sql_upsert}/classifier.py +4 -4
  46. {streamgate-0.4.0/src/streamgate/contrib/sql_sink → streamgate-1.0.0/src/streamgate/contrib/sql_upsert}/config.py +2 -2
  47. {streamgate-0.4.0/src/streamgate/contrib/sql_sink → streamgate-1.0.0/src/streamgate/contrib/sql_upsert}/engines.py +1 -1
  48. streamgate-1.0.0/src/streamgate/contrib/sql_upsert/factory.py +77 -0
  49. {streamgate-0.4.0/src/streamgate/contrib/sql_sink → streamgate-1.0.0/src/streamgate/contrib/sql_upsert}/upsert.py +72 -38
  50. streamgate-1.0.0/src/streamgate/contrib/sqlite_upsert/__init__.py +72 -0
  51. streamgate-1.0.0/src/streamgate/ingest/dedup/in_memory.py +123 -0
  52. streamgate-1.0.0/src/streamgate/ingest/producer.py +842 -0
  53. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/obs/metrics.py +7 -7
  54. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/protocols.py +113 -113
  55. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/resilience/health.py +61 -60
  56. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/transport/kafka.py +29 -20
  57. streamgate-0.4.0/CHANGELOG.md +0 -218
  58. streamgate-0.4.0/CONFIGURATION.md +0 -139
  59. streamgate-0.4.0/PKG-INFO +0 -202
  60. streamgate-0.4.0/README.md +0 -166
  61. streamgate-0.4.0/README.zh-CN.md +0 -311
  62. streamgate-0.4.0/examples/http_probe/consume.py +0 -53
  63. streamgate-0.4.0/examples/prod_pipeline/consume.py +0 -96
  64. streamgate-0.4.0/examples/prod_pipeline/produce.py +0 -103
  65. streamgate-0.4.0/examples/pure_pipeline/README.md +0 -61
  66. streamgate-0.4.0/examples/pure_pipeline/consume.py +0 -80
  67. streamgate-0.4.0/examples/pure_pipeline/produce.py +0 -69
  68. streamgate-0.4.0/examples/redis_admission/README.md +0 -50
  69. streamgate-0.4.0/examples/redis_admission/produce.py +0 -84
  70. streamgate-0.4.0/examples/sqlite_sink/README.md +0 -53
  71. streamgate-0.4.0/examples/sqlite_sink/consume.py +0 -90
  72. streamgate-0.4.0/examples/sqlite_sink/models.py +0 -22
  73. streamgate-0.4.0/examples/sqlite_sink/produce.py +0 -58
  74. streamgate-0.4.0/src/streamgate/consumer/runner.py +0 -251
  75. streamgate-0.4.0/src/streamgate/contrib/mssql_sink/__init__.py +0 -30
  76. streamgate-0.4.0/src/streamgate/contrib/redis_admission/__init__.py +0 -39
  77. streamgate-0.4.0/src/streamgate/contrib/redis_admission/admission.py +0 -487
  78. streamgate-0.4.0/src/streamgate/contrib/redis_admission/config.py +0 -18
  79. streamgate-0.4.0/src/streamgate/contrib/redis_admission/existence.py +0 -236
  80. streamgate-0.4.0/src/streamgate/contrib/sql_sink/__init__.py +0 -36
  81. streamgate-0.4.0/src/streamgate/contrib/sqlite_sink/__init__.py +0 -29
  82. streamgate-0.4.0/src/streamgate/ingest/admission/in_memory.py +0 -84
  83. streamgate-0.4.0/src/streamgate/ingest/admission/no_admission.py +0 -37
  84. streamgate-0.4.0/src/streamgate/ingest/gateway.py +0 -310
  85. streamgate-0.4.0/src/streamgate/ingest/producer.py +0 -299
  86. streamgate-0.4.0/src/streamgate/specs.py +0 -90
  87. {streamgate-0.4.0 → streamgate-1.0.0}/.github/workflows/release.yml +0 -0
  88. {streamgate-0.4.0 → streamgate-1.0.0}/.gitignore +0 -0
  89. {streamgate-0.4.0 → streamgate-1.0.0}/AGENTS.md +0 -0
  90. {streamgate-0.4.0 → streamgate-1.0.0}/CONTRIBUTING.md +0 -0
  91. {streamgate-0.4.0 → streamgate-1.0.0}/LICENSE +0 -0
  92. {streamgate-0.4.0 → streamgate-1.0.0}/examples/docker-compose.yml +0 -0
  93. {streamgate-0.4.0 → streamgate-1.0.0}/examples/http_probe/README.md +0 -0
  94. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/consumer/__init__.py +0 -0
  95. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/contrib/_deps.py +0 -0
  96. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/contrib/http_probe/__init__.py +0 -0
  97. {streamgate-0.4.0/src/streamgate/contrib/sql_sink → streamgate-1.0.0/src/streamgate/contrib/sql_upsert}/_dialects/mssql.py +0 -0
  98. {streamgate-0.4.0/src/streamgate/contrib/sql_sink → streamgate-1.0.0/src/streamgate/contrib/sql_upsert}/_dialects/sqlite.py +0 -0
  99. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/ingest/__init__.py +0 -0
  100. {streamgate-0.4.0/src/streamgate/ingest/admission → streamgate-1.0.0/src/streamgate/ingest/dedup}/__init__.py +0 -0
  101. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/obs/__init__.py +0 -0
  102. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/obs/logging.py +0 -0
  103. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/resilience/__init__.py +0 -0
  104. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/resilience/backpressure.py +0 -0
  105. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/transport/__init__.py +0 -0
  106. {streamgate-0.4.0 → streamgate-1.0.0}/src/streamgate/transport/codec.py +0 -0
  107. {streamgate-0.4.0 → streamgate-1.0.0}/uv.lock +0 -0
@@ -100,15 +100,15 @@ jobs:
100
100
  assert streamgate.__version__
101
101
 
102
102
  from streamgate import (
103
- ConsumeSpec, DefaultErrorClassifier, ErrorKind,
103
+ Consumer, ConsumerOptions, DefaultErrorClassifier, ErrorKind,
104
104
  InMemoryAdmission, IngestBinding, IngestGateway,
105
105
  ManualBackpressureSignal,
106
106
  )
107
107
 
108
108
  try:
109
- ConsumeSpec()
110
- except ValueError as e:
111
- assert "sink" in str(e) and "on_record" in str(e)
109
+ Consumer()
110
+ except TypeError as e:
111
+ assert "handler" in str(e)
112
112
 
113
113
  print("bare-install import smoke OK")
114
114
  EOF
@@ -0,0 +1,499 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-09-10
9
+
10
+ ### BREAKING — both sides redesigned as flat entry points
11
+
12
+ **Consumer side — a "data outlet".** The consumer API stopped modeling
13
+ "persistence" and now models what it always was: a consume loop. You hand it a
14
+ handler; offsets, retries, self-healing, poison isolation and graceful
15
+ shutdown are the framework's job. Single-record vs. batch is just
16
+ `batch_size=1` vs `batch_size=N`.
17
+
18
+ **Producer side — a "door".** The producer API stopped modeling the
19
+ "entity + slot" business scenario and the two-hop `IngestBinding →
20
+ IngestGateway` assembly. `Producer(bootstrap_servers, topic, key, options)`
21
+ is the whole entry: dedup is opt-in (`DedupOptions`, one line for the
22
+ in-process carrier), backpressure folds into `options`, and the routing key
23
+ (`key=`) is separated from the identity key (`DedupOptions.key`) — they were
24
+ welded together in 0.x and that welding was the root cause of every awkward
25
+ semantic.
26
+
27
+ **Migration guide (old → new):**
28
+
29
+ Constructing the consumer:
30
+
31
+ ```text
32
+ old: ConsumeSpec(on_record=h) + ConsumerWorker(spec, kafka_config=..., consumer_config=...)
33
+ new: Consumer(bootstrap_servers=..., topic=..., group_id=..., handler=h)
34
+
35
+ old: ConsumeSpec(sink=UpsertWriter(...)) + ConsumerWorker(..., error_classifier=...)
36
+ new (out-of-the-box): SqliteConsumer(db=..., upserts=[...], topic=..., group_id=...)
37
+ new (self-assembled): Consumer(..., handler=h, probe=p,
38
+ options=ConsumerOptions(classifier=...))
39
+
40
+ old: batch_size=1 (on ConsumerConfig)
41
+ new: Consumer(..., batch_size=1)
42
+ ```
43
+
44
+ Constructing the producer:
45
+
46
+ ```text
47
+ old: IngestBinding(...) + IngestGateway(binding=..., kafka_config=..., backpressure_config=...)
48
+ new: Producer(bootstrap_servers=..., topic=..., options=ProducerOptions(...))
49
+
50
+ old: gateway.process(record, overwrite=True)
51
+ new: producer.push(record, force=True)
52
+
53
+ old: IngestBinding(admission="in-memory", entity_key=..., slot_key=..., summary=...)
54
+ new: ProducerOptions(dedup=DedupOptions(key=..., summary=...)) # in-process carrier
55
+
56
+ old: IngestBinding(admission="none")
57
+ new: omit dedup entirely (pure push, zero dedup concepts)
58
+
59
+ old: outcome = await gateway.process(record); outcome.kind == "conflict"
60
+ new: result = await producer.push(record); result.kind == "duplicate"
61
+ ```
62
+
63
+ API renames / removals (consumer side):
64
+
65
+ | 0.x | 1.0.0 |
66
+ |-----|-------|
67
+ | `ConsumerWorker(spec, kafka_config=..., consumer_config=..., ...)` | `Consumer(bootstrap_servers, topic, group_id, handler, batch_size, flush_timeout, options)` |
68
+ | `ConsumeSpec` (deleted) | flat constructor arguments + `ConsumerOptions` |
69
+ | `RecordWriter` protocol + `WriteResult` (retired) | `BatchHandler = (batch, context) -> None` — the only outlet contract |
70
+ | `RecordHandler` alias | `BatchHandler` |
71
+ | — | `Probe = (record) -> None` (single-record probe for precise DLQ location) |
72
+ | `ConsumerConfig` (deleted) | flat arguments + `ConsumerOptions.tuning` / `options.dlq` |
73
+ | `ConsumeSpec.expected_message_type` | `ConsumerOptions.expected_type` |
74
+ | `ConsumeSpec.persist_policy` | `ConsumerOptions.persist_hook` |
75
+ | `ConsumeSpec.collapse_key` / `log_context` | `ConsumerOptions.collapse_key` / `log_context` |
76
+ | `ConsumeSpec.dlq` / `dlq_topic` / `dlq_message_type` | `ConsumerOptions.dlq.enabled` / `.topic` / `.message_type` |
77
+ | `ConsumerWorker.error_classifier` | `ConsumerOptions.classifier` |
78
+ | `ConsumerWorker.codec` | `ConsumerOptions.codec` |
79
+ | `ConsumerWorker.cache` | `ConsumerOptions.health_probe` |
80
+ | `ConsumerWorker.existence_ttl_seconds` | `ConsumerOptions.backlog_ttl_seconds` |
81
+ | `ConsumerWorker.metrics_config` | `ConsumerOptions.metrics_window_seconds` |
82
+ | `ConsumerConfig.batch_timeout_seconds` | `flush_timeout` argument / `CONSUMER__FLUSH_TIMEOUT_SECONDS` |
83
+ | `ConsumerConfig.reconnect_base_backoff_seconds` / `reconnect_max_backoff_seconds` | `RuntimeTuning.reconnect_base` / `.reconnect_max` |
84
+ | `ConsumerConfig.backlog_check_interval_seconds` | `RuntimeTuning.backlog_check_interval` |
85
+ | `KafkaConfig.dlq_topic` | `DlqOptions.topic` / env `KAFKA__DLQ_TOPIC` |
86
+ | `BisectOutcome.written` | `BisectOutcome.handled` |
87
+ | `locate_and_write(writer, ...)` | `locate_and_quarantine(probe, ...)` |
88
+ | `DlqProducer(kafka_config, ...)` | `DlqProducer(bootstrap_servers, topic=..., send_retries=..., ...)` |
89
+ | `KafkaConsumerService(kafka_config, consumer_config, topic)` | `KafkaConsumerService(bootstrap_servers, topic, group_id, auto_offset_reset=..., ...)` |
90
+
91
+ API renames / removals (producer side):
92
+
93
+ | 0.x | 1.0.0 |
94
+ |-----|-------|
95
+ | `IngestBinding` (deleted) | flat `Producer` constructor arguments + `ProducerOptions` |
96
+ | `IngestGateway(binding=...)` | `Producer(bootstrap_servers, topic, key, options)` |
97
+ | `IngestGateway.process()` | `Producer.push()` |
98
+ | `IngestOutcome` / `OutcomeKind` | `PushResult` / `PushKind` |
99
+ | `OutcomeKind.CONFLICT` | `PushKind.DUPLICATE` |
100
+ | `OutcomeKind.KAFKA_UNAVAILABLE` | `PushKind.UNAVAILABLE` |
101
+ | `IngestGateway.process(overwrite=True)` / `IngestBinding.is_overwrite` | `Producer.push(force=True)` (per-call, not per-record) |
102
+ | `IngestBinding.entity_key` + `slot_key` | split: `Producer(key=...)` = routing key (ordering); `DedupOptions(key=...)` = identity key (dedup) |
103
+ | `IngestBinding.partition_key` | `Producer(key=...)` |
104
+ | `IngestBinding.message_type` | `ProducerOptions.message_type` (default `streamgate_record`) |
105
+ | `IngestBinding.summary` | `DedupOptions.summary` |
106
+ | `IngestBinding.admission="in-memory"` | `ProducerOptions(dedup=DedupOptions(key=...))` (built-in in-process carrier) |
107
+ | `IngestBinding.admission=<AdmissionPolicy>` | `DedupOptions(carrier=<DedupCarrier>)` |
108
+ | `IngestBinding.log_context` | `ProducerOptions.log_context` |
109
+ | `IngestBinding.request_log_context` | removed (HTTP presentation contract — map it in your adapter) |
110
+ | `IngestBinding.backpressure_error_codes` / `backpressure_default_code` / `backpressure_detail` | removed (core only returns `kind`; HTTP mapping is yours) |
111
+ | `IngestBinding.kafka_unavailable_code` / `kafka_unavailable_detail` | removed (`PushResult.reason` only) |
112
+ | `IngestOutcome.status_code` / `error_code` / `detail` | removed — `PushResult` carries `kind` + `reason` + `retry_after` only |
113
+ | `IngestGateway.health()` → `IngestHealthResponse` | `Producer.health()` → `ProducerHealthResponse` |
114
+ | `AdmissionPolicy` protocol | `DedupCarrier` protocol (same hooks; `RejectInfo` carries `reason` + `retry_after`, no HTTP codes; `on_overwrite_accepted` renamed `on_force_accepted`; new `guarantee` property, self-reported) |
115
+ | `RejectInfo.status_code` / `error_code` / `detail` | removed — `RejectInfo(reason, retry_after, ...)` |
116
+ | `DecisionKind.CONFLICT` | `DecisionKind.DUPLICATE` |
117
+ | `InMemoryAdmission` / `NoAdmission` | built-in `InMemoryDedupCarrier` (auto-selected); `dedup=None` replaces `NoAdmission` |
118
+ | `KafkaProducerService` | internalized into `Producer` (self-healing preserved, class no longer exported) |
119
+ | `streamgate.contrib.redis_admission` | `streamgate.contrib.redis_dedup` |
120
+ | `RedisExistenceAdmission` | `RedisDedupCarrier` (`guarantee="distributed"`; `entity_key`/`slot_key` → single `key`; query endpoint `entity_slots` removed) |
121
+ | `RedisExistenceAdmissionConfig` | `RedisDedupCarrierConfig` (error-code fields removed) |
122
+ | `RedisExistenceCache` | `RedisDedupCache` (identity→summary STRING keys, single-key Lua reserve) |
123
+ | `RedisConfig.existence_ttl_seconds` | `RedisConfig.identity_ttl_seconds` (empty-entity sentinel removed — no entity grouping) |
124
+ | `BackfillSource.load(entity) -> dict[slot, summary]` | `BackfillSource.load(identity) -> summary \| None` |
125
+ | `SqlBackfill(entity_column=..., slot_column=...)` | `SqlBackfill(key_column=...)` (single identity key) |
126
+ | `IngestMetrics` | `ProducerMetrics` (`admission_conflict` → `duplicate`) |
127
+ | `collect_ingest_health` | `collect_producer_health` |
128
+
129
+ Environment variables:
130
+
131
+ | 0.x | 1.0.0 |
132
+ |-----|-------|
133
+ | `CONSUMER__BATCH_TIMEOUT_SECONDS` | renamed to `CONSUMER__FLUSH_TIMEOUT_SECONDS` |
134
+ | all other `KAFKA__*` / `CONSUMER__*` / `METRICS__*` keys | unchanged (now read directly by `Producer` / `Consumer` as fallbacks: `KAFKA__BOOTSTRAP_SERVERS`, `KAFKA__TOPIC`, `CONSUMER__GROUP_ID`, `CONSUMER__BATCH_SIZE`, DLQ, tuning and metrics-window keys) |
135
+
136
+ Health-snapshot metrics:
137
+
138
+ | 0.x | 1.0.0 |
139
+ |-----|-------|
140
+ | `ConsumerHealthResponse.database` | `output` (outlet connectivity; outlet without an observable carrier reads `connected`) |
141
+ | `sink_write_rate` | `handle_rate` |
142
+ | `sink_write_failure_rate` | `handle_failure_rate` |
143
+ | `sink_write_latency_ms_avg` / `_max` | `handle_latency_ms_avg` / `_max` |
144
+ | `consume_rate` / `retry_rate` | unchanged |
145
+ | `IngestHealthResponse.receive_rate` | `ProducerHealthResponse.push_rate` |
146
+ | `IngestHealthResponse.admission_conflict_rate` | `ProducerHealthResponse.duplicate_rate` |
147
+
148
+ Log events (both sides keep stable names; producer-side field renames only):
149
+
150
+ | 0.x | 1.0.0 |
151
+ |-----|-------|
152
+ | `batch_write_start` | `batch_handle_start` |
153
+ | `batch_write_success` | `batch_handle_success` |
154
+ | `batch_write_failed` | `batch_handle_failed` |
155
+ | `batch_write_exhausted_retries` | `batch_handle_exhausted_retries` |
156
+ | `batch_write_success_with_quarantine` | `batch_handle_success_with_quarantine` |
157
+ | `consumer_paused_due_to_write_failures` | `consumer_paused_due_to_handle_failures` |
158
+ | `offset_commit_failed_after_write` | `offset_commit_failed_after_handle` |
159
+ | `write_failure_classified` | `handle_failure_classified` |
160
+ | `batch_bisect_triggered` / `bisect_aborted_probe_failed` / `consumer_record_quarantined` | unchanged |
161
+ | `existence_ttl_seconds` log field | `backlog_ttl_seconds` |
162
+ | `ingest_request` field `overwrite` | field `force` (event name unchanged) |
163
+ | `admission_send_success_hook_failed` / `admission_send_failed_hook_failed` | `dedup_send_success_hook_failed` / `dedup_send_failed_hook_failed` |
164
+ | `overwrite_rejected_redis_unavailable` / `overwrite_summary_write_failed` | `force_rejected_redis_unavailable` / `force_summary_write_failed` |
165
+ | `load_entity_failed` / `load_entity_failed_degraded` / `cold_path_db_load` | `load_identity_failed` / `load_identity_failed_degraded` / `cold_path_load` |
166
+ | `ingest_request` / `ingest_conflict` / `ingest_rejected_backpressure` / `kafka_*` | unchanged (the `error_code` field was dropped from push-path rejections) |
167
+
168
+ Import paths:
169
+
170
+ | 0.x | 1.0.0 |
171
+ |-----|-------|
172
+ | `streamgate.contrib.sql_sink` | `streamgate.contrib.sql_upsert` (base: `Upsert`, `upsert_outlet`, engine factory, `SqlBackfill`, `SQLAlchemyErrorClassifier`, `DbConfig`) |
173
+ | `streamgate.contrib.sqlite_sink` | `streamgate.contrib.sqlite_upsert` (exports `SqliteConsumer` factory) |
174
+ | `streamgate.contrib.mssql_sink` | `streamgate.contrib.mssql_upsert` (exports `MssqlConsumer` factory) |
175
+ | `UpsertWriter` (retired) | `upsert_outlet(db, upserts) -> tuple[BatchHandler, Probe]` |
176
+ | `streamgate.ingest.gateway` / `streamgate.specs` / `streamgate.ingest.admission` | deleted — use `streamgate.Producer` / `ProducerOptions` / `DedupOptions` |
177
+ | `streamgate.contrib.redis_admission` | `streamgate.contrib.redis_dedup` |
178
+
179
+ No compatibility shims are provided (consistent with a major boundary);
180
+ passing a retired keyword to `Consumer` or `Producer` raises a `TypeError`
181
+ that points to this section.
182
+
183
+ ### Added
184
+
185
+ - **`Consumer`** — the flat, embeddable consume loop: four required arguments
186
+ answer "which cluster / which topic / which identity / where data goes";
187
+ `batch_size` (default 500) and `flush_timeout` (default 5.0s) are
188
+ first-class; everything else lives in `ConsumerOptions` (with nested
189
+ `DlqOptions` and `RuntimeTuning`) and defaults to zero conceptual load.
190
+ Required settings fall back to environment variables (12-factor):
191
+ `KAFKA__BOOTSTRAP_SERVERS` / `KAFKA__TOPIC` / `CONSUMER__GROUP_ID` (the
192
+ handler is code-only by design).
193
+ - **`Producer`** — the flat, embeddable push entry: two required arguments
194
+ (`bootstrap_servers`, `topic`, both with `KAFKA__*` env fallbacks) plus an
195
+ optional routing `key` (same key ⇒ same partition ⇒ ordered; omitted ⇒
196
+ round-robin, unordered — documented promise). `push(record, force=False,
197
+ source=...)` returns a transport-neutral `PushResult` (`accepted` /
198
+ `duplicate` / `rejected` / `backpressure` / `unavailable`) and never raises
199
+ runtime exceptions for degraded dependencies. Dedup is opt-in via
200
+ `ProducerOptions(dedup=DedupOptions(key=...))` — one line enables the
201
+ built-in in-process carrier (`guarantee="process-local"`); injecting a
202
+ `DedupCarrier` (e.g. `contrib.redis_dedup.RedisDedupCarrier`,
203
+ `guarantee="distributed"`) scales the guarantee with your topology. The
204
+ carrier self-reports its guarantee strength and it surfaces as
205
+ `PushResult.guarantee` on every result. Rejection windows mean zero writes:
206
+ no reservation, no shared-store write, no Kafka send. The Kafka
207
+ connection's self-healing monitor (probing, single-flight rebuild,
208
+ exponential backoff) is preserved internally — no separate class to manage.
209
+ - **Single-record probe (`ConsumerOptions.probe`)** — neutral primitive for
210
+ precise poison isolation: when a handler raises a POISON-classified error
211
+ and a probe is provided, the framework re-runs the probe per record
212
+ (with reference comparison to distinguish data poison from outlet
213
+ failure); probe-successful records count as handled and commit, probe-
214
+ failing records are quarantined to the DLQ individually. Without a probe
215
+ the whole batch is quarantined (offsets still commit, no poison retries).
216
+ With the DLQ disabled the legacy paused self-healing applies.
217
+ - **`SqliteConsumer` / `MssqlConsumer` factories** — pre-assembled `Consumer`s
218
+ for the SQL outlet: batch upsert handler + single-record probe + dialect
219
+ error classifier wired, user `options` merged field-by-field over factory
220
+ defaults. They return the core `Consumer` (no subclass hierarchy). SQLite
221
+ auto-creates tables at start (unchanged); MSSQL schema stays with your
222
+ migration tool.
223
+ - Old parameter names passed to `Consumer` or `Producer` fail with a
224
+ `TypeError` carrying a migration hint.
225
+
226
+ ### Changed
227
+
228
+ - Poison handling is probe-based instead of write-batch bisection: location
229
+ calls the user probe record-by-record with reference verification; the
230
+ safety invariants are unchanged (probe-failed entries are never
231
+ quarantined; a suspected outlet failure aborts location and pauses —
232
+ handled entries are idempotently re-processed on the next pass).
233
+ - `ConsumerHealthResponse` / `ConsumeMetrics` vocabulary neutralized to
234
+ handle-rate/handle-latency; the producer side follows:
235
+ `IngestHealthResponse` → `ProducerHealthResponse`,
236
+ `receive_rate` → `push_rate`, `admission_conflict_rate` → `duplicate_rate`
237
+ (mapping tables above).
238
+ - Handler objects (callables implementing `__call__`) may opt into framework-
239
+ managed lifecycle via duck-typed `start()` / `close()` and into the health
240
+ snapshot via `check_health()` (used by the SQL outlet to report
241
+ `output` connectivity).
242
+ - Dedup carriers and backpressure signals implementing `start()`/`close()`
243
+ get framework-managed lifecycles (same duck-typed contract as consumer
244
+ hooks).
245
+
246
+ ### Removed
247
+
248
+ - `ConsumeSpec`, `RecordWriter`, `WriteResult`, `RecordHandler`,
249
+ `ConsumerWorker`, `ConsumerConfig`, `KafkaConfig.dlq_topic`,
250
+ `contrib.sql_sink` / `contrib.sqlite_sink` / `contrib.mssql_sink`
251
+ (renamed — see import mapping above).
252
+ - Producer side: `IngestBinding`, `IngestGateway`, `IngestOutcome`,
253
+ `OutcomeKind`, `KafkaProducerService`, `InMemoryAdmission`, `NoAdmission`,
254
+ the `streamgate.ingest.admission` package, `streamgate.specs`,
255
+ `IngestRecordT`, and the HTTP-presentation contracts
256
+ (`status_code` / `error_code` / `detail` fields on results and rejects,
257
+ `backpressure_error_codes` / `backpressure_default_code` /
258
+ `backpressure_detail` / `kafka_unavailable_code` /
259
+ `kafka_unavailable_detail` / `request_log_context`).
260
+ - `contrib.redis_admission` (renamed to `contrib.redis_dedup` — see import
261
+ mapping above; the per-entity query endpoint `entity_slots` and the
262
+ empty-entity sentinel were not carried over: the single-identity-key model
263
+ has no entity grouping).
264
+
265
+ ## [0.5.0] - 2026-09-10
266
+
267
+ ### Added
268
+
269
+ - **`AdmissionPolicy` gains `on_send_success` / `on_send_failed` hooks.**
270
+ `on_send_success(record)` fires after every broker-acked Kafka send
271
+ (overwrite path included); `on_send_failed(record)` fires when a send
272
+ fails after the admission reservation was placed. The framework default
273
+ (no-op) preserves current behavior — the placeholder stays reserved and
274
+ self-heals via TTL/backfill — but implementations can now release the
275
+ reservation to make the key immediately re-submittable, accepting the
276
+ duplicate risk of ambiguous (timeout) failures.
277
+
278
+ ### Changed
279
+
280
+ - **`AdmissionPolicy.on_accepted` renamed to `on_overwrite_accepted`.**
281
+ The old hook only fired on the overwrite path (not on every accepted
282
+ send, despite the name); the new name states that. Custom policies that
283
+ implement `on_accepted` keep working: the gateway falls back to it when
284
+ `on_overwrite_accepted` is absent. Built-in policies provide both
285
+ (the old name delegates to the new one).
286
+
287
+ ## [0.4.0] - 2026-09-08
288
+
289
+ ### Added
290
+
291
+ - **Health snapshots now carry ops rate/latency metrics.** Both
292
+ `IngestHealthResponse` and `ConsumerHealthResponse` gain sliding-window
293
+ fields (all defaulting to `0.0`, computed on read over a 1-second bucket
294
+ sliding window — no background tasks, no extra endpoint):
295
+ - ingest: `receive_rate`, `admission_conflict_rate`,
296
+ `backpressure_reject_rate`, `produce_success_rate`,
297
+ `produce_failure_rate`, `produce_latency_ms_avg`,
298
+ `produce_latency_ms_max`;
299
+ - consume: `consume_rate`, `sink_write_rate`, `sink_write_failure_rate`,
300
+ `retry_rate`, `sink_write_latency_ms_avg`, `sink_write_latency_ms_max`.
301
+ Rates are records/second, latencies milliseconds; the window length is
302
+ configurable via the new `MetricsConfig` (`METRICS__WINDOW_SECONDS`,
303
+ default `60`, range 1–600). `IngestGateway(metrics_config=...)` and
304
+ `ConsumerWorker(metrics_config=...)` accept it; when omitted everything
305
+ works with the default window. Purely additive — old consumers that
306
+ deserialize the snapshot ignore the new fields.
307
+ - **`MetricsConfig`** exported from the package root.
308
+
309
+ ### Changed
310
+
311
+ - `ConsumeRuntime.__init__`: the structured-metrics sink parameter
312
+ `metrics` was renamed to `metrics_sink` to free the `metrics` name for
313
+ the new rate-metrics container (`ConsumeMetrics`). No in-repo caller
314
+ passed it explicitly; external callers using `metrics=<MetricsSink>`
315
+ must rename the keyword.
316
+
317
+ ## [0.3.1] - 2026-09-08
318
+
319
+ ### Fixed
320
+
321
+ - **`ConsumerWorker` now manages the `ConsumeSpec.persist_policy` lifecycle**
322
+ (the `AdmissionPolicy` protocol promises "the framework guarantees call
323
+ ordering", but the consumer entry point never honored it — unlike
324
+ `IngestGateway`, which calls `start()`/`close()`). `prepare()` now calls
325
+ `await policy.start()` before starting the sink, and `shutdown()` calls
326
+ `await policy.close()` before closing the writer. Previously the consumer
327
+ side never started the policy, so `on_persisted` authoritative refresh /
328
+ TTL heartbeats failed on every batch (e.g. `RedisExistenceAdmission`
329
+ logging `cache_refresh_failed: RedisExistenceCache not started`) and the
330
+ failure surfaced only as per-record WARN logs. Lifecycle calls are
331
+ duck-typed (defensive `getattr`) so legacy policy implementations without
332
+ `start()`/`close()` keep working.
333
+ - **`ConsumerWorker(cache=...)` lifecycle is now symmetric.** The bypass
334
+ `cache` parameter (health-probe component) is started in `prepare()` when
335
+ it implements `start()`, instead of being closed at shutdown without ever
336
+ having been started. `RedisExistenceCache.start()`/`close()` are
337
+ idempotent, so sharing one cache instance between the policy and the
338
+ bypass parameter remains safe (double start/close is a no-op).
339
+ The pre-fix workaround (`await cache.start()` before `worker.run()`)
340
+ is no longer required.
341
+
342
+ ## [0.3.0] - 2026-09-08
343
+
344
+ **Contrib release.** The production-grade I/O strategies that 0.2.0 demoted to
345
+ copy-paste examples are back in the wheel — as `streamgate.contrib.*`,
346
+ importable, opt-in subpackages. The core stays pure: it gains no dependencies,
347
+ and it still never imports contrib (the import-linter forbidden contract is
348
+ replaced by a layered contract with the same guarantee). Purely additive; no
349
+ breaking changes.
350
+
351
+ ### Added
352
+
353
+ - **`streamgate.contrib`** — official strategy layer (provisional / beta
354
+ grade; ships in the wheel, third-party deps via extras, friendly
355
+ "install streamgate[...]" errors when an extra is missing):
356
+ - `contrib.redis_admission` (extra `[redis]`): `RedisExistenceAdmission`,
357
+ `RedisExistenceCache`, `RedisConfig` — shared-storage uniqueness
358
+ admission with atomic Lua reservation, TTL idle-GC, empty-entity
359
+ sentinels, fail-closed semantics.
360
+ - `contrib.sql_sink` / `contrib.sqlite_sink` / `contrib.mssql_sink`
361
+ (extra `[sql]`): `Upsert`/`UpsertWriter` (`RecordWriter` impl),
362
+ engine factory, `SqlBackfill` (`BackfillSource` impl),
363
+ `SQLAlchemyErrorClassifier`, `DbConfig` — SQLite `ON CONFLICT` and
364
+ MSSQL multi-row `MERGE` + HOLDLOCK dialects.
365
+ - `contrib.http_probe` (extra `[http]`): `HttpProbeSignal` /
366
+ `HysteresisController` — backlog-age trip/recover hysteresis,
367
+ fail-closed probing, kafka-down fast reject.
368
+ - **Extras** `[redis]` / `[sql]` / `[http]` (new names; the pre-0.2.0 extras
369
+ `[sqlite]` / `[mssql]` / `[http-probe]` / `[all]` are not restored).
370
+ - **`examples/prod_pipeline/`** — production-topology demo wiring the three
371
+ contrib components together (Redis uniqueness admission + HTTP-probe
372
+ backpressure + MSSQL idempotent sink; runs on SQLite locally via
373
+ connection-string dialect selection).
374
+
375
+ ### Changed
376
+
377
+ - **import-linter**: the "pure core" forbidden-modules contract is replaced
378
+ by a layered contract — `streamgate.contrib` sits on top and may use the
379
+ core; no core layer may import contrib. Bare-install purity is still
380
+ CI-gated (bare install pulls exactly `aiokafka`/`loguru`/`pydantic`).
381
+ - **`examples/`**: the three strategy examples now import from
382
+ `streamgate.contrib`; demo scripts (produce/consume/health_server/models)
383
+ stay in the repo only. `pure_pipeline/` remains the zero-dependency main
384
+ example and is unchanged.
385
+ - **Dev group** now depends on `streamgate[redis,sql,http]` instead of
386
+ duplicating the third-party libs, keeping a single source of truth for
387
+ contrib dependency versions.
388
+
389
+ ## [0.2.0] - 2026-09-07
390
+
391
+ **Pure-core release.** All database, Redis and HTTP-client code is removed from
392
+ the wheel; every I/O strategy is now a typed protocol hook with a copy-paste
393
+ reference implementation in `examples/`. Core dependencies collapse to
394
+ `aiokafka` + `loguru` + `pydantic`. This is a breaking release (0.1.0 was
395
+ published days earlier; the migration cost is intentionally accepted).
396
+
397
+ ### Changed
398
+
399
+ - **Core dependencies**: `aiokafka` / `loguru` / `pydantic` only. The
400
+ `sqlalchemy` and `sqlmodel` dependencies — and the entire extras system
401
+ (`[sqlite]`, `[mssql]`, `[redis]`, `[http-probe]`, `[all]`) — are gone.
402
+ - **`ConsumeSpec`**: landing targets reduced to two, mutually exclusive:
403
+ `sink` (a `RecordWriter` — the only persistence path) or `on_record`
404
+ (escape hatch). Providing neither — or both — fails fast at declaration
405
+ with fix guidance.
406
+ - **`IngestBinding.admission`**: string shortcuts are now `"none"` and
407
+ `"in-memory"` (new built-in, zero-I/O, single-process). `"redis-existence"`
408
+ is removed — inject a `RedisExistenceAdmission` instance instead
409
+ (copy from `examples/redis_admission/`).
410
+ - **`IngestGateway`**: default backpressure signal is now the built-in
411
+ `ManualBackpressureSignal` (static switch, defaults to not rejecting);
412
+ `db_config` / `redis_config` constructor parameters removed.
413
+ - **`ConsumerWorker`**: constructor parameters `db_config`, `redis_config`,
414
+ `writer`, `engine`, `session_factory` removed — the sink comes from
415
+ `ConsumeSpec.sink`. New `error_classifier` parameter (see below).
416
+
417
+ ### Added
418
+
419
+ - **`ErrorClassifier` protocol + `ErrorKind` enum** (`RETRY` / `POISON` /
420
+ `FATAL`): write-failure classification is now an injected policy.
421
+ `DefaultErrorClassifier` only recognizes generic exceptions
422
+ (timeouts / KafkaError → retry; ValueError → poison; unknown → poison,
423
+ probe-protected bisect). Consumers writing to a database **must** inject a
424
+ DB-aware classifier — `examples/sqlite_sink/classifier.py` ships a
425
+ production-hardened `SQLAlchemyErrorClassifier` (incl. SQL Server error
426
+ numbers).
427
+ - **`ManualBackpressureSignal`**: built-in pure in-memory backpressure switch.
428
+ - **`InMemoryAdmission`**: built-in zero-I/O uniqueness admission
429
+ (single-process only; multi-instance topologies should inject a
430
+ shared-storage policy).
431
+ - **`examples/`** (not part of the wheel, CI-gated for lint/typecheck):
432
+ - `pure_pipeline/` — bare-install full round trip (main example; the
433
+ living proof of `pip install streamgate` purity)
434
+ - `sqlite_sink/` — the former `db/` package (engines / upsert sugar /
435
+ backfill / SQLite+MSSQL dialects) as local modules + `RecordWriter`
436
+ injection + `SQLAlchemyErrorClassifier`
437
+ - `redis_admission/` — the former `cache/existence.py` +
438
+ `ingest/admission/redis_existence.py` + `AdmissionPolicy` injection
439
+ - `http_probe/` — the former `HttpProbeSignal` (hysteresis state machine) +
440
+ `BackpressureSignal` injection
441
+
442
+ ### Removed
443
+
444
+ - `streamgate.db` package (`UpsertWriter`, `SqlBackfill`, engine factories,
445
+ SQLite/MSSQL dialects), `Upsert` declaration, `DbConfig` / `RedisConfig`
446
+ (and their `DB__*` / `REDIS__*` env mappings), `RedisExistenceCache`,
447
+ `RedisExistenceAdmission` (+ related symbols), `HttpProbeSignal`,
448
+ `_optional` lazy-loading gate.
449
+ - `FailureCategory` / `add_failure_rule` / `classify_write_failure`
450
+ (superseded by `ErrorClassifier`).
451
+
452
+ ### Migration Guide (0.1.0 → 0.2.0)
453
+
454
+ | 0.1.0 symbol | Where it went |
455
+ |--------------|---------------|
456
+ | `Upsert` | `examples/sqlite_sink/upsert.py` (`Upsert`, local class) |
457
+ | `UpsertWriter` | `examples/sqlite_sink/upsert.py` — inject via `ConsumeSpec(sink=UpsertWriter(...))` |
458
+ | `DbConfig`, `create_read_engine`, `create_write_engine`, `async_session_factory` | `examples/sqlite_sink/config.py` + `engines.py` |
459
+ | `SqlBackfill` | `examples/sqlite_sink/backfill.py` |
460
+ | SQLite / MSSQL dialect internals | `examples/sqlite_sink/dialects/` |
461
+ | `RedisExistenceCache`, `EMPTY_FIELD` | `examples/redis_admission/existence.py` |
462
+ | `RedisExistenceAdmission`, `RedisExistenceAdmissionConfig`, `EntitySlots`, `SlotSource`, `ExistenceUnavailableError`, `UndeterminedReason` | `examples/redis_admission/admission.py` — inject via `IngestBinding(admission=RedisExistenceAdmission(...))` |
463
+ | `HttpProbeSignal` | `examples/http_probe/probe_signal.py` — inject via `IngestGateway(signal=HttpProbeSignal(...))` |
464
+ | `FailureCategory`, `classify_write_failure`, `add_failure_rule` | `ErrorKind` + `ErrorClassifier`; DB mapping in `examples/sqlite_sink/classifier.py` |
465
+ | `ConsumeSpec(upserts=[...])` | `ConsumeSpec(sink=UpsertWriter(..., [Upsert(...)]))` (writer built by you) |
466
+ | `ConsumerWorker(db_config=..., spec=ConsumeSpec(upserts=...))` | `ConsumerWorker(spec, ...)` with `ConsumeSpec(sink=...)`; pass `error_classifier=` |
467
+ | `admission="redis-existence"` | `admission=RedisExistenceAdmission(...)` (instance injection) |
468
+ | `pip install "streamgate[redis]"` etc. | `pip install streamgate <third-party-lib>` + copy the example module |
469
+
470
+ ## [0.1.0] - 2026-09-07
471
+
472
+ First public release. Data pipeline framework: conditional admission → reliable delivery → pluggable sinks.
473
+
474
+ ### Added
475
+
476
+ - **Ingest (admission)**
477
+ - `IngestGateway`: transport-agnostic ingest kernel — backpressure check → admission → Kafka send → on_accepted orchestration.
478
+ - `IngestBinding`: declarative ingest binding (Tier 0) with `admission="none" | "redis-existence"` shortcuts or a custom `AdmissionPolicy`.
479
+ - `RedisExistenceAdmission` / `RedisExistenceCache` (extras: `[redis]`): uniqueness contract via existence check + atomic slot reservation (cluster-safe Lua) + TTL idle-GC + empty-entity sentinels + fail-closed semantics; optional cold-entity DB backfill via `BackfillSource` (`SqlBackfill`).
480
+ - **Transport**
481
+ - `KafkaProducerService` / `KafkaConsumerService` over aiokafka: lazy connect, health probing, self-healing reconnects with exponential backoff and rebuild thresholds.
482
+ - `JsonEnvelopeCodec` envelope format; custom `MessageCodec` supported.
483
+ - **Consumer (sink)**
484
+ - `ConsumerWorker` + `consume_loop`: batch buffering, at-least-once processing, graceful shutdown.
485
+ - `ConsumeSpec` (Tier 0) with three mutually exclusive landing targets: `upserts` (built-in idempotent SQL upserts via `Upsert`), `sink` (`RecordWriter` injection point), `on_record` (no-sink escape hatch).
486
+ - Built-in SQLite / SQL Server upsert dialects (extras: `[sqlite]` / `[mssql]`); other databases via Tier 1 `RecordWriter`.
487
+ - DLQ bisection: poison records are located by batch bisection and quarantined to a dead-letter topic instead of wedging the partition.
488
+ - **Resilience & observability**
489
+ - Consumption backpressure with hysteresis (trip/recover on backlog age), fail-closed probing, `HttpProbeSignal` (extras: `[http-probe]`); custom `BackpressureSignal` supported.
490
+ - Health snapshots for both processes (`collect_ingest_health` / `collect_consumer_health`); exposure is left to the caller (the package contains no web-framework code).
491
+ - Structured logging via loguru (`configure_logger`, `logger`) and a pluggable `MetricsSink`.
492
+ - **Packaging**
493
+ - Mechanisms as core dependencies; all policy carriers (DB drivers, redis, httpx) as optional extras with lazy loading and install-guidance errors.
494
+ - Typed configuration objects mapping 1:1 to `KAFKA__*` / `CONSUMER__*` / `DB__*` / `REDIS__*` / `BACKPRESSURE__*` environment variables; required settings fail fast at startup.
495
+
496
+ [0.5.0]: https://github.com/pwg-code/streamgate/releases/tag/v0.5.0
497
+ [0.4.0]: https://github.com/pwg-code/streamgate/releases/tag/v0.4.0
498
+ [0.2.0]: https://github.com/pwg-code/streamgate/releases/tag/v0.2.0
499
+ [0.1.0]: https://github.com/pwg-code/streamgate/releases/tag/v0.1.0