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