streamgate 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. streamgate-0.1.0/.gitignore +26 -0
  2. streamgate-0.1.0/.import_linter_cache/.gitignore +2 -0
  3. streamgate-0.1.0/.import_linter_cache/5780e4798d429798e55d962d9844c38a9b5e0494.data.json +1 -0
  4. streamgate-0.1.0/.import_linter_cache/CACHEDIR.TAG +3 -0
  5. streamgate-0.1.0/.import_linter_cache/streamgate.meta.json +1 -0
  6. streamgate-0.1.0/AGENTS.md +12 -0
  7. streamgate-0.1.0/LICENSE +21 -0
  8. streamgate-0.1.0/PKG-INFO +189 -0
  9. streamgate-0.1.0/README.md +146 -0
  10. streamgate-0.1.0/pyproject.toml +111 -0
  11. streamgate-0.1.0/src/streamgate/__init__.py +214 -0
  12. streamgate-0.1.0/src/streamgate/_optional.py +38 -0
  13. streamgate-0.1.0/src/streamgate/cache/__init__.py +0 -0
  14. streamgate-0.1.0/src/streamgate/cache/existence.py +233 -0
  15. streamgate-0.1.0/src/streamgate/config.py +150 -0
  16. streamgate-0.1.0/src/streamgate/consumer/__init__.py +0 -0
  17. streamgate-0.1.0/src/streamgate/consumer/classifier.py +139 -0
  18. streamgate-0.1.0/src/streamgate/consumer/dlq.py +375 -0
  19. streamgate-0.1.0/src/streamgate/consumer/loop.py +649 -0
  20. streamgate-0.1.0/src/streamgate/consumer/runner.py +233 -0
  21. streamgate-0.1.0/src/streamgate/db/__init__.py +0 -0
  22. streamgate-0.1.0/src/streamgate/db/backfill.py +145 -0
  23. streamgate-0.1.0/src/streamgate/db/dialects/__init__.py +0 -0
  24. streamgate-0.1.0/src/streamgate/db/dialects/mssql.py +126 -0
  25. streamgate-0.1.0/src/streamgate/db/dialects/sqlite.py +57 -0
  26. streamgate-0.1.0/src/streamgate/db/engines.py +113 -0
  27. streamgate-0.1.0/src/streamgate/db/upsert.py +205 -0
  28. streamgate-0.1.0/src/streamgate/ingest/__init__.py +0 -0
  29. streamgate-0.1.0/src/streamgate/ingest/admission/__init__.py +0 -0
  30. streamgate-0.1.0/src/streamgate/ingest/admission/no_admission.py +37 -0
  31. streamgate-0.1.0/src/streamgate/ingest/admission/redis_existence.py +489 -0
  32. streamgate-0.1.0/src/streamgate/ingest/gateway.py +328 -0
  33. streamgate-0.1.0/src/streamgate/ingest/producer.py +299 -0
  34. streamgate-0.1.0/src/streamgate/obs/__init__.py +0 -0
  35. streamgate-0.1.0/src/streamgate/obs/logging.py +43 -0
  36. streamgate-0.1.0/src/streamgate/obs/metrics.py +30 -0
  37. streamgate-0.1.0/src/streamgate/protocols.py +355 -0
  38. streamgate-0.1.0/src/streamgate/resilience/__init__.py +0 -0
  39. streamgate-0.1.0/src/streamgate/resilience/backpressure.py +370 -0
  40. streamgate-0.1.0/src/streamgate/resilience/health.py +266 -0
  41. streamgate-0.1.0/src/streamgate/specs.py +133 -0
  42. streamgate-0.1.0/src/streamgate/transport/__init__.py +0 -0
  43. streamgate-0.1.0/src/streamgate/transport/codec.py +62 -0
  44. streamgate-0.1.0/src/streamgate/transport/kafka.py +194 -0
  45. streamgate-0.1.0/uv.lock +920 -0
@@ -0,0 +1,26 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+
13
+ # Tooling caches
14
+ .ruff_cache/
15
+ .pyright/
16
+ .mypy_cache/
17
+ .pytest_cache/
18
+ uv.lock.bak
19
+
20
+ # Environment
21
+ .env
22
+ .env.*
23
+
24
+ # Local data (sqlite default path) and planning scratch space
25
+ data/
26
+ temp/
@@ -0,0 +1,2 @@
1
+ # Automatically created by Grimp.
2
+ *
@@ -0,0 +1 @@
1
+ {"imports_by_module":{"streamgate":[["streamgate.obs.metrics",false,59,"from streamgate.obs.metrics import LoggingMetricsSink, MetricsSink"],["streamgate.consumer.loop",false,46,"from streamgate.consumer.loop import ConsumeRuntime, consume_loop"],["streamgate.specs",false,87,"from streamgate.specs import ConsumeSpec, IngestBinding, IngestRecordT, Upsert"],["streamgate.ingest.producer",false,57,"from streamgate.ingest.producer import KafkaProducerService"],["streamgate.protocols",false,60,"from streamgate.protocols import ("],["streamgate.consumer.runner",false,47,"from streamgate.consumer.runner import ConsumerWorker"],["streamgate.resilience.backpressure",false,102,"from streamgate.resilience.backpressure import HttpProbeSignal"],["streamgate.consumer.dlq",false,38,"from streamgate.consumer.dlq import ("],["streamgate.ingest.admission.no_admission",false,55,"from streamgate.ingest.admission.no_admission import NoAdmission"],["streamgate.ingest.admission.redis_existence",false,94,"from streamgate.ingest.admission.redis_existence import ("],["streamgate._optional",false,25,"from streamgate._optional import require_optional"],["streamgate.db.backfill",false,48,"from streamgate.db.backfill import SqlBackfill"],["streamgate.obs.logging",false,58,"from streamgate.obs.logging import configure_logger, logger"],["streamgate.db.upsert",false,54,"from streamgate.db.upsert import UpsertWriter"],["streamgate.consumer.classifier",false,33,"from streamgate.consumer.classifier import ("],["streamgate.transport.kafka",false,89,"from streamgate.transport.kafka import KafkaConsumerService"],["streamgate.db.engines",false,49,"from streamgate.db.engines import ("],["streamgate.resilience.health",false,81,"from streamgate.resilience.health import ("],["streamgate.ingest.gateway",false,56,"from streamgate.ingest.gateway import IngestGateway"],["streamgate.cache.existence",false,93,"from streamgate.cache.existence import EMPTY_FIELD, RedisExistenceCache"],["streamgate.config",false,26,"from streamgate.config import ("],["streamgate.transport.codec",false,88,"from streamgate.transport.codec import JsonEnvelopeCodec"]],"streamgate._optional":[],"streamgate.cache":[],"streamgate.cache.existence":[["streamgate.obs.logging",false,16,"from streamgate.obs.logging import logger"],["streamgate._optional",false,14,"from streamgate._optional import require_optional"],["streamgate.config",false,15,"from streamgate.config import RedisConfig"]],"streamgate.config":[],"streamgate.consumer":[],"streamgate.consumer.classifier":[],"streamgate.consumer.dlq":[["streamgate.obs.logging",false,24,"from streamgate.obs.logging import logger"],["streamgate.protocols",false,25,"from streamgate.protocols import JsonObject, RecordWriter"],["streamgate.config",false,22,"from streamgate.config import KafkaConfig"],["streamgate.consumer.classifier",false,23,"from streamgate.consumer.classifier import FailureCategory"]],"streamgate.consumer.loop":[["streamgate.consumer.dlq",false,18,"from streamgate.consumer.dlq import ("],["streamgate.cache.existence",false,12,"from streamgate.cache.existence import RedisExistenceCache"],["streamgate.obs.logging",false,27,"from streamgate.obs.logging import logger"],["streamgate.consumer.classifier",false,14,"from streamgate.consumer.classifier import ("],["streamgate.protocols",false,29,"from streamgate.protocols import ("],["streamgate.transport.kafka",false,38,"from streamgate.transport.kafka import KafkaConsumerService, KafkaRecord"],["streamgate.specs",false,37,"from streamgate.specs import ConsumeSpec"],["streamgate.config",false,13,"from streamgate.config import ConsumerConfig"],["streamgate.obs.metrics",false,28,"from streamgate.obs.metrics import DEFAULT_METRICS, MetricsSink"]],"streamgate.consumer.runner":[["streamgate.obs.logging",false,23,"from streamgate.obs.logging import logger"],["streamgate.db.upsert",false,22,"from streamgate.db.upsert import UpsertWriter"],["streamgate.db.engines",false,21,"from streamgate.db.engines import async_session_factory, create_write_engine"],["streamgate.config",false,18,"from streamgate.config import ConsumerConfig, DbConfig, KafkaConfig, RedisConfig"],["streamgate.specs",false,29,"from streamgate.specs import ConsumeSpec"],["streamgate.cache.existence",false,17,"from streamgate.cache.existence import RedisExistenceCache"],["streamgate.transport.kafka",false,31,"from streamgate.transport.kafka import KafkaConsumerService"],["streamgate.protocols",false,24,"from streamgate.protocols import MessageCodec, RecordWriter"],["streamgate.consumer.dlq",false,19,"from streamgate.consumer.dlq import DlqProducer"],["streamgate.consumer.loop",false,20,"from streamgate.consumer.loop import ConsumeRuntime, consume_loop"],["streamgate.transport.codec",false,30,"from streamgate.transport.codec import JsonEnvelopeCodec"],["streamgate.resilience.health",false,25,"from streamgate.resilience.health import ("]],"streamgate.db":[],"streamgate.db.backfill":[["streamgate.protocols",false,10,"from streamgate.protocols import JsonObject"],["streamgate.obs.logging",false,9,"from streamgate.obs.logging import logger"]],"streamgate.db.dialects":[],"streamgate.db.dialects.mssql":[["streamgate.protocols",false,21,"from streamgate.protocols import JsonObject"]],"streamgate.db.dialects.sqlite":[["streamgate.protocols",false,8,"from streamgate.protocols import JsonObject"]],"streamgate.db.engines":[["streamgate.obs.logging",false,12,"from streamgate.obs.logging import logger"],["streamgate.config",false,11,"from streamgate.config import DbConfig"]],"streamgate.db.upsert":[["streamgate.config",false,13,"from streamgate.config import DbConfig"],["streamgate.db.dialects.mssql",false,14,"from streamgate.db.dialects import mssql as mssql_dialect"],["streamgate.protocols",false,17,"from streamgate.protocols import JsonObject, WriteResult"],["streamgate.specs",false,18,"from streamgate.specs import Upsert"],["streamgate.obs.logging",false,16,"from streamgate.obs.logging import logger"],["streamgate.db.dialects.sqlite",false,15,"from streamgate.db.dialects import sqlite as sqlite_dialect"]],"streamgate.ingest":[],"streamgate.ingest.admission":[],"streamgate.ingest.admission.no_admission":[["streamgate.protocols",false,5,"from streamgate.protocols import Decision, RecordT"]],"streamgate.ingest.admission.redis_existence":[["streamgate.cache.existence",false,16,"from streamgate.cache.existence import RedisExistenceCache"],["streamgate.protocols",false,19,"from streamgate.protocols import ("],["streamgate.obs.logging",false,18,"from streamgate.obs.logging import logger"],["streamgate.config",false,17,"from streamgate.config import RedisConfig"]],"streamgate.ingest.gateway":[["streamgate.cache.existence",false,11,"from streamgate.cache.existence import RedisExistenceCache"],["streamgate.protocols",false,20,"from streamgate.protocols import ("],["streamgate.obs.logging",false,19,"from streamgate.obs.logging import logger"],["streamgate.ingest.producer",false,18,"from streamgate.ingest.producer import KafkaProducerService"],["streamgate.resilience.health",false,30,"from streamgate.resilience.health import ("],["streamgate.ingest.admission.no_admission",false,13,"from streamgate.ingest.admission.no_admission import NoAdmission"],["streamgate.ingest.admission.redis_existence",false,14,"from streamgate.ingest.admission.redis_existence import ("],["streamgate.specs",false,34,"from streamgate.specs import IngestBinding, IngestRecordT"],["streamgate.resilience.backpressure",false,29,"from streamgate.resilience.backpressure import HttpProbeSignal"],["streamgate.config",false,12,"from streamgate.config import BackpressureConfig, DbConfig, KafkaConfig, RedisConfig"],["streamgate.transport.codec",false,35,"from streamgate.transport.codec import JsonEnvelopeCodec"]],"streamgate.ingest.producer":[["streamgate.protocols",false,18,"from streamgate.protocols import JsonObject"],["streamgate.obs.logging",false,17,"from streamgate.obs.logging import logger"],["streamgate.config",false,16,"from streamgate.config import KafkaConfig"]],"streamgate.obs":[],"streamgate.obs.logging":[],"streamgate.obs.metrics":[["streamgate.obs.logging",false,10,"from streamgate.obs.logging import logger"]],"streamgate.protocols":[],"streamgate.resilience":[],"streamgate.resilience.backpressure":[["streamgate.obs.logging",false,27,"from streamgate.obs.logging import logger"],["streamgate._optional",false,25,"from streamgate._optional import require_optional"],["streamgate.config",false,26,"from streamgate.config import BackpressureConfig"],["streamgate.protocols",false,28,"from streamgate.protocols import BackpressureSnapshot, ProbeResult"]],"streamgate.resilience.health":[["streamgate.obs.logging",false,15,"from streamgate.obs.logging import logger"]],"streamgate.specs":[["streamgate.protocols",false,18,"from streamgate.protocols import ("],["streamgate.db.dialects.mssql",false,17,"from streamgate.db.dialects.mssql import mssql_cast_types"]],"streamgate.transport":[],"streamgate.transport.codec":[["streamgate.protocols",false,11,"from streamgate.protocols import Envelope, JsonObject"],["streamgate.obs.logging",false,10,"from streamgate.obs.logging import logger"]],"streamgate.transport.kafka":[["streamgate.obs.logging",false,11,"from streamgate.obs.logging import logger"],["streamgate.config",false,10,"from streamgate.config import ConsumerConfig, KafkaConfig"]]},"version":2}
@@ -0,0 +1,3 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
2
+ # This file is a cache directory tag automatically created by Grimp.
3
+ # For information about cache directory tags see https://bford.info/cachedir/
@@ -0,0 +1 @@
1
+ {"streamgate.db.dialects": 1788742270.5761602, "streamgate": 1788776206.1352222, "streamgate.ingest": 1788742270.5761602, "streamgate.config": 1788775744.4866724, "streamgate.transport": 1788742270.5761602, "streamgate._optional": 1788775681.4141757, "streamgate.consumer.runner": 1788767058.740163, "streamgate.db.engines": 1788775754.7772968, "streamgate.db.dialects.sqlite": 1788776206.140225, "streamgate.resilience": 1788742270.5761602, "streamgate.ingest.admission": 1788742270.5761602, "streamgate.db.dialects.mssql": 1788749506.9339142, "streamgate.transport.kafka": 1788776206.140225, "streamgate.db.upsert": 1788766960.2905881, "streamgate.specs": 1788766931.7234573, "streamgate.resilience.health": 1788767769.120218, "streamgate.db.backfill": 1788749357.442897, "streamgate.obs.logging": 1788749262.353655, "streamgate.consumer.classifier": 1788776206.138224, "streamgate.ingest.admission.no_admission": 1788759910.7492158, "streamgate.cache.existence": 1788775707.6089175, "streamgate.consumer.loop": 1788767712.111625, "streamgate.consumer.dlq": 1788750409.4867704, "streamgate.consumer": 1788742270.5761602, "streamgate.ingest.gateway": 1788767162.2992928, "streamgate.transport.codec": 1788749619.0487723, "streamgate.db": 1788742270.5761602, "streamgate.protocols": 1788766898.5526717, "streamgate.obs.metrics": 1788749264.9746833, "streamgate.obs": 1788742270.5761602, "streamgate.cache": 1788742270.5761602, "streamgate.ingest.admission.redis_existence": 1788776250.1590097, "streamgate.ingest.producer": 1788776206.139223, "streamgate.resilience.backpressure": 1788775727.538583}
@@ -0,0 +1,12 @@
1
+ # AGENTS.md
2
+
3
+ ## 架构与编码规范
4
+
5
+ **类型与 Pydantic v2**:
6
+ - 所有函数/方法必须标注参数与返回值类型,禁止省略。
7
+ - 禁用 `Any`、裸 `dict` / `list`(无泛型)等无法推断具体结构的类型;类型不确定时用具体类型或 `object` + `isinstance` 守卫。所有结构化数据必须定义对应的 Schema(Pydantic Model / dataclass / TypedDict)。
8
+ - 使用 Python 3.12+ 语法:`A | B`(代替 `Union`)、`list[int]`(代替 `List[int]`);可能返回空值的函数必须标注 `| None`。
9
+ - 单函数体不超过 50 行,复杂逻辑拆分为带类型注解的子函数;生成代码时先输出数据模型定义,再输出业务逻辑。
10
+ - 禁止手动逐字段映射:用 `model_validate()`(ORM 对象加 `from_attributes=True`)代替 `_to_response()` 和 `Schema(**obj)`;用 `model_dump(exclude=.../exclude_unset=True)` 代替手写字典;字段定义只出现在 Schema 一处。响应体 Schema 有默认值,请求体 Schema 保持必填。
11
+ - 新增代码必须通过 pyright 静态检查。
12
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wanguo Pang
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,189 @@
1
+ Metadata-Version: 2.5
2
+ Name: streamgate
3
+ Version: 0.1.0
4
+ Summary: A Kafka-backed data pipeline framework: conditional admission, reliable delivery, pluggable sinks.
5
+ Project-URL: Homepage, https://github.com/{github_owner}/streamgate
6
+ Project-URL: Repository, https://github.com/{github_owner}/streamgate
7
+ Project-URL: Changelog, https://github.com/{github_owner}/streamgate/blob/main/CHANGELOG.md
8
+ Author: Wanguo Pang
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backpressure,dlq,ingestion,kafka,pipeline,redis,sqlalchemy
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: System :: Distributed Computing
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: aiokafka>=0.10
25
+ Requires-Dist: loguru>=0.7
26
+ Requires-Dist: pydantic>=2.5
27
+ Requires-Dist: sqlalchemy>=2.0
28
+ Requires-Dist: sqlmodel>=0.0.16
29
+ Provides-Extra: all
30
+ Requires-Dist: aioodbc>=0.5; extra == 'all'
31
+ Requires-Dist: aiosqlite>=0.19; extra == 'all'
32
+ Requires-Dist: httpx>=0.27; extra == 'all'
33
+ Requires-Dist: redis>=5; extra == 'all'
34
+ Provides-Extra: http-probe
35
+ Requires-Dist: httpx>=0.27; extra == 'http-probe'
36
+ Provides-Extra: mssql
37
+ Requires-Dist: aioodbc>=0.5; extra == 'mssql'
38
+ Provides-Extra: redis
39
+ Requires-Dist: redis>=5; extra == 'redis'
40
+ Provides-Extra: sqlite
41
+ Requires-Dist: aiosqlite>=0.19; extra == 'sqlite'
42
+ Description-Content-Type: text/markdown
43
+
44
+ # streamgate
45
+
46
+ [![PyPI version](https://img.shields.io/pypi/v/streamgate.svg)](https://pypi.org/project/streamgate/)
47
+ [![Python versions](https://img.shields.io/pypi/pyversions/streamgate.svg)](https://pypi.org/project/streamgate/)
48
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/{github_owner}/streamgate/blob/main/LICENSE)
49
+ [![CI](https://github.com/{github_owner}/streamgate/actions/workflows/ci.yml/badge.svg)](https://github.com/{github_owner}/streamgate/actions/workflows/ci.yml)
50
+
51
+ **streamgate** is a Kafka-backed data pipeline framework: conditional admission at the door, reliable delivery through Kafka, and idempotent sinking wherever you want the data to land.
52
+
53
+ - 中文提示:本仓库对外文档以英文为主;内部代码注释保留中文。
54
+
55
+ ## Why streamgate
56
+
57
+ - **Mechanisms in the framework, policies in your hands.** The framework owns admission plumbing, backpressure, health probing, reconnection, offset handling and graceful shutdown. You own entity/slot semantics, schemas and sink targets.
58
+ - **A uniqueness contract, not just "send to Kafka".** Existence checks + atomic slot reservation (Lua, cluster-safe) + cold-path backfill from your database turn at-least-once ingestion into at-most-once admission (409 conflicts instead of duplicates).
59
+ - **Backpressure with hysteresis.** Ingest probes consumer health on a side channel and starts/stop rejecting based on backlog age, with separate trip/recover thresholds so flapping states settle.
60
+ - **DLQ bisection instead of head-of-line blocking.** A poison message never wedges a partition: the failing batch is bisected, the bad record is quarantined to a dead-letter topic, the rest commits.
61
+ - **Tier 0 declarative API covers ~90% of use cases.** Declare what you ingest and where it lands; drop to protocols and escape hatches only when you must.
62
+
63
+ ## Quick Start
64
+
65
+ ```python
66
+ import asyncio
67
+ from pydantic import BaseModel
68
+ from sqlmodel import SQLModel, Field
69
+
70
+ from streamgate import (
71
+ BackpressureConfig, ConsumeSpec, ConsumerConfig, ConsumerWorker,
72
+ DbConfig, IngestBinding, IngestGateway, KafkaConfig, Upsert,
73
+ )
74
+
75
+ KAFKA = KafkaConfig(bootstrap_servers="localhost:9092", topic="orders")
76
+
77
+ class OrderIn(BaseModel): # ingress schema (policy lives with you)
78
+ order_id: str
79
+ amount: float
80
+
81
+ class Order(SQLModel, table=True): # persistence model
82
+ order_id: str = Field(primary_key=True)
83
+ amount: float
84
+
85
+ binding = IngestBinding(
86
+ message_type="order",
87
+ entity_key=lambda r: r.order_id,
88
+ slot_key=lambda r: "order",
89
+ summary=lambda r: {"amount": r.amount},
90
+ admission="none", # or "redis-existence" (extras: streamgate[redis])
91
+ )
92
+
93
+ async def ingest_one(record: OrderIn) -> None:
94
+ gateway = IngestGateway(binding=binding, kafka_config=KAFKA,
95
+ backpressure_config=BackpressureConfig(enabled=False))
96
+ await gateway.start()
97
+ outcome = await gateway.process(record)
98
+ await gateway.close()
99
+ print(outcome.kind)
100
+
101
+ async def consume() -> None:
102
+ spec = ConsumeSpec(upserts=[Upsert(model=Order, keys=["order_id"])])
103
+ worker = ConsumerWorker(
104
+ spec, kafka_config=KAFKA,
105
+ consumer_config=ConsumerConfig(group_id="order-sink"),
106
+ db_config=DbConfig(connection_string="sqlite+aiosqlite:///./data/streamgate.db"),
107
+ )
108
+ await worker.run() # blocks until SIGINT/SIGTERM
109
+ ```
110
+
111
+ A runnable version of this (producer + consumer scripts, docker-compose included) lives in [`examples/`](examples/).
112
+
113
+ ## Core concepts
114
+
115
+ | Tier | What you use | When |
116
+ |------|--------------|------|
117
+ | **0 — Declarative** | `IngestBinding` + `ConsumeSpec`/`Upsert` | ~90% of cases: declare message type, uniqueness keys, sink target |
118
+ | **1 — Component swap** | Protocols in `streamgate.protocols` + built-ins | Replace admission, sinks, backpressure, codec, backfill with your own |
119
+ | **2 — Escape hatch** | `ConsumeSpec.on_record` / `ConsumeContext` | Handle records yourself; no sink machinery at all |
120
+
121
+ Key protocols (all in `streamgate.protocols`):
122
+
123
+ - `AdmissionPolicy` — decide admit / 409-conflict / reject per record before it reaches Kafka.
124
+ - `RecordWriter` — where consumed batches land (database, search index, another service...).
125
+ - `BackpressureSignal` — tells the gateway whether to accept; the built-in HTTP probe is just one implementation.
126
+
127
+ **Stability:** v0.x, experimental. The protocols module is the frozen contract; everything else may still shift.
128
+
129
+ ## Architecture
130
+
131
+ ```
132
+ your HTTP app (presentation is yours: auth/routing/OpenAPI)
133
+
134
+
135
+ IngestGateway ──► backpressure signal (side channel) ─┐
136
+ │ admission: existence check + │
137
+ │ atomic slot reservation (+DB │
138
+ │ backfill on cold entities) │
139
+ ▼ │
140
+ Kafka ─────────────────────────────────────────────┘ DLQ ◄─ poisoned records
141
+ │ (bisection)
142
+
143
+ ConsumerWorker ──► sink / upserts / on_record ──► your storage
144
+
145
+ └─ health snapshot (expose it with your own web framework)
146
+ ```
147
+
148
+ The package contains **zero web-framework code** (no fastapi/uvicorn): HTTP presentation belongs to your adapter layer.
149
+
150
+ ## Installation
151
+
152
+ ```bash
153
+ pip install streamgate # pure Kafka pipeline: no DB, no redis
154
+ ```
155
+
156
+ Mechanisms are core; policy carriers are extras:
157
+
158
+ | Extra | Installs | Enables |
159
+ |-------|----------|---------|
160
+ | `[sqlite]` | aiosqlite | built-in upserts / backfill against SQLite |
161
+ | `[mssql]` | aioodbc | built-in upserts / backfill against SQL Server |
162
+ | `[redis]` | redis | `redis-existence` admission strategy |
163
+ | `[http-probe]` | httpx | built-in HTTP backpressure probe signal |
164
+ | `[all]` | all of the above | everything built-in |
165
+
166
+ ```bash
167
+ pip install "streamgate[sqlite]" # built-in persistence (Quick Start above)
168
+ pip install "streamgate[all]" # full built-in capability
169
+ ```
170
+
171
+ Other databases (PostgreSQL, MySQL, ...): inject your own `RecordWriter` (Tier 1). The built-in upsert sugar targets SQLite and SQL Server semantics.
172
+
173
+ ## Configuration
174
+
175
+ All components configure via typed objects that map 1:1 to environment variables (`KAFKA__*`, `CONSUMER__*`, `DB__*`, `REDIS__*`, `BACKPRESSURE__*`). Required settings (e.g. the Kafka topic, consumer group id, and the DB connection string when upserts are declared) fail fast at startup with fix instructions. See [CONFIGURATION.md](CONFIGURATION.md).
176
+
177
+ ## Examples
178
+
179
+ See [`examples/`](examples/) for a complete ingest → consume → SQLite round trip with a `docker-compose.yml` for Kafka + Redis.
180
+
181
+ ## Roadmap
182
+
183
+ - Test suite (first release ships without tests; APIs are exercised in production but the project considers this its top debt)
184
+ - Documentation site
185
+ - More admission policies and sink writers
186
+
187
+ ## License
188
+
189
+ [MIT](LICENSE)
@@ -0,0 +1,146 @@
1
+ # streamgate
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/streamgate.svg)](https://pypi.org/project/streamgate/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/streamgate.svg)](https://pypi.org/project/streamgate/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/{github_owner}/streamgate/blob/main/LICENSE)
6
+ [![CI](https://github.com/{github_owner}/streamgate/actions/workflows/ci.yml/badge.svg)](https://github.com/{github_owner}/streamgate/actions/workflows/ci.yml)
7
+
8
+ **streamgate** is a Kafka-backed data pipeline framework: conditional admission at the door, reliable delivery through Kafka, and idempotent sinking wherever you want the data to land.
9
+
10
+ - 中文提示:本仓库对外文档以英文为主;内部代码注释保留中文。
11
+
12
+ ## Why streamgate
13
+
14
+ - **Mechanisms in the framework, policies in your hands.** The framework owns admission plumbing, backpressure, health probing, reconnection, offset handling and graceful shutdown. You own entity/slot semantics, schemas and sink targets.
15
+ - **A uniqueness contract, not just "send to Kafka".** Existence checks + atomic slot reservation (Lua, cluster-safe) + cold-path backfill from your database turn at-least-once ingestion into at-most-once admission (409 conflicts instead of duplicates).
16
+ - **Backpressure with hysteresis.** Ingest probes consumer health on a side channel and starts/stop rejecting based on backlog age, with separate trip/recover thresholds so flapping states settle.
17
+ - **DLQ bisection instead of head-of-line blocking.** A poison message never wedges a partition: the failing batch is bisected, the bad record is quarantined to a dead-letter topic, the rest commits.
18
+ - **Tier 0 declarative API covers ~90% of use cases.** Declare what you ingest and where it lands; drop to protocols and escape hatches only when you must.
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ import asyncio
24
+ from pydantic import BaseModel
25
+ from sqlmodel import SQLModel, Field
26
+
27
+ from streamgate import (
28
+ BackpressureConfig, ConsumeSpec, ConsumerConfig, ConsumerWorker,
29
+ DbConfig, IngestBinding, IngestGateway, KafkaConfig, Upsert,
30
+ )
31
+
32
+ KAFKA = KafkaConfig(bootstrap_servers="localhost:9092", topic="orders")
33
+
34
+ class OrderIn(BaseModel): # ingress schema (policy lives with you)
35
+ order_id: str
36
+ amount: float
37
+
38
+ class Order(SQLModel, table=True): # persistence model
39
+ order_id: str = Field(primary_key=True)
40
+ amount: float
41
+
42
+ binding = IngestBinding(
43
+ message_type="order",
44
+ entity_key=lambda r: r.order_id,
45
+ slot_key=lambda r: "order",
46
+ summary=lambda r: {"amount": r.amount},
47
+ admission="none", # or "redis-existence" (extras: streamgate[redis])
48
+ )
49
+
50
+ async def ingest_one(record: OrderIn) -> None:
51
+ gateway = IngestGateway(binding=binding, kafka_config=KAFKA,
52
+ backpressure_config=BackpressureConfig(enabled=False))
53
+ await gateway.start()
54
+ outcome = await gateway.process(record)
55
+ await gateway.close()
56
+ print(outcome.kind)
57
+
58
+ async def consume() -> None:
59
+ spec = ConsumeSpec(upserts=[Upsert(model=Order, keys=["order_id"])])
60
+ worker = ConsumerWorker(
61
+ spec, kafka_config=KAFKA,
62
+ consumer_config=ConsumerConfig(group_id="order-sink"),
63
+ db_config=DbConfig(connection_string="sqlite+aiosqlite:///./data/streamgate.db"),
64
+ )
65
+ await worker.run() # blocks until SIGINT/SIGTERM
66
+ ```
67
+
68
+ A runnable version of this (producer + consumer scripts, docker-compose included) lives in [`examples/`](examples/).
69
+
70
+ ## Core concepts
71
+
72
+ | Tier | What you use | When |
73
+ |------|--------------|------|
74
+ | **0 — Declarative** | `IngestBinding` + `ConsumeSpec`/`Upsert` | ~90% of cases: declare message type, uniqueness keys, sink target |
75
+ | **1 — Component swap** | Protocols in `streamgate.protocols` + built-ins | Replace admission, sinks, backpressure, codec, backfill with your own |
76
+ | **2 — Escape hatch** | `ConsumeSpec.on_record` / `ConsumeContext` | Handle records yourself; no sink machinery at all |
77
+
78
+ Key protocols (all in `streamgate.protocols`):
79
+
80
+ - `AdmissionPolicy` — decide admit / 409-conflict / reject per record before it reaches Kafka.
81
+ - `RecordWriter` — where consumed batches land (database, search index, another service...).
82
+ - `BackpressureSignal` — tells the gateway whether to accept; the built-in HTTP probe is just one implementation.
83
+
84
+ **Stability:** v0.x, experimental. The protocols module is the frozen contract; everything else may still shift.
85
+
86
+ ## Architecture
87
+
88
+ ```
89
+ your HTTP app (presentation is yours: auth/routing/OpenAPI)
90
+
91
+
92
+ IngestGateway ──► backpressure signal (side channel) ─┐
93
+ │ admission: existence check + │
94
+ │ atomic slot reservation (+DB │
95
+ │ backfill on cold entities) │
96
+ ▼ │
97
+ Kafka ─────────────────────────────────────────────┘ DLQ ◄─ poisoned records
98
+ │ (bisection)
99
+
100
+ ConsumerWorker ──► sink / upserts / on_record ──► your storage
101
+
102
+ └─ health snapshot (expose it with your own web framework)
103
+ ```
104
+
105
+ The package contains **zero web-framework code** (no fastapi/uvicorn): HTTP presentation belongs to your adapter layer.
106
+
107
+ ## Installation
108
+
109
+ ```bash
110
+ pip install streamgate # pure Kafka pipeline: no DB, no redis
111
+ ```
112
+
113
+ Mechanisms are core; policy carriers are extras:
114
+
115
+ | Extra | Installs | Enables |
116
+ |-------|----------|---------|
117
+ | `[sqlite]` | aiosqlite | built-in upserts / backfill against SQLite |
118
+ | `[mssql]` | aioodbc | built-in upserts / backfill against SQL Server |
119
+ | `[redis]` | redis | `redis-existence` admission strategy |
120
+ | `[http-probe]` | httpx | built-in HTTP backpressure probe signal |
121
+ | `[all]` | all of the above | everything built-in |
122
+
123
+ ```bash
124
+ pip install "streamgate[sqlite]" # built-in persistence (Quick Start above)
125
+ pip install "streamgate[all]" # full built-in capability
126
+ ```
127
+
128
+ Other databases (PostgreSQL, MySQL, ...): inject your own `RecordWriter` (Tier 1). The built-in upsert sugar targets SQLite and SQL Server semantics.
129
+
130
+ ## Configuration
131
+
132
+ All components configure via typed objects that map 1:1 to environment variables (`KAFKA__*`, `CONSUMER__*`, `DB__*`, `REDIS__*`, `BACKPRESSURE__*`). Required settings (e.g. the Kafka topic, consumer group id, and the DB connection string when upserts are declared) fail fast at startup with fix instructions. See [CONFIGURATION.md](CONFIGURATION.md).
133
+
134
+ ## Examples
135
+
136
+ See [`examples/`](examples/) for a complete ingest → consume → SQLite round trip with a `docker-compose.yml` for Kafka + Redis.
137
+
138
+ ## Roadmap
139
+
140
+ - Test suite (first release ships without tests; APIs are exercised in production but the project considers this its top debt)
141
+ - Documentation site
142
+ - More admission policies and sink writers
143
+
144
+ ## License
145
+
146
+ [MIT](LICENSE)
@@ -0,0 +1,111 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "streamgate"
7
+ dynamic = ["version"]
8
+ description = "A Kafka-backed data pipeline framework: conditional admission, reliable delivery, pluggable sinks."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Wanguo Pang" }]
13
+ keywords = [
14
+ "kafka",
15
+ "pipeline",
16
+ "ingestion",
17
+ "backpressure",
18
+ "dlq",
19
+ "sqlalchemy",
20
+ "redis",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Topic :: Database",
32
+ "Topic :: System :: Distributed Computing",
33
+ "Typing :: Typed",
34
+ ]
35
+ # 核心依赖只含"机制":传输(aiokafka)、观测(loguru)、配置(pydantic)、
36
+ # Upsert sugar 载体(sqlalchemy + sqlmodel)。
37
+ # 数据库驱动 / redis / httpx 全部为 extras(策略实现按需安装)。
38
+ dependencies = [
39
+ "aiokafka>=0.10",
40
+ "loguru>=0.7",
41
+ "pydantic>=2.5",
42
+ "sqlalchemy>=2.0",
43
+ "sqlmodel>=0.0.16",
44
+ ]
45
+
46
+ [project.optional-dependencies]
47
+ sqlite = ["aiosqlite>=0.19"]
48
+ mssql = ["aioodbc>=0.5"]
49
+ redis = ["redis>=5"]
50
+ http-probe = ["httpx>=0.27"]
51
+ all = [
52
+ "aiosqlite>=0.19",
53
+ "aioodbc>=0.5",
54
+ "httpx>=0.27",
55
+ "redis>=5",
56
+ ]
57
+
58
+ [project.urls]
59
+ Homepage = "https://github.com/{github_owner}/streamgate"
60
+ Repository = "https://github.com/{github_owner}/streamgate"
61
+ Changelog = "https://github.com/{github_owner}/streamgate/blob/main/CHANGELOG.md"
62
+
63
+ [tool.hatch.version]
64
+ path = "src/streamgate/__init__.py"
65
+
66
+ [tool.hatch.build.targets.wheel]
67
+ packages = ["src/streamgate"]
68
+
69
+ [dependency-groups]
70
+ dev = [
71
+ # 可选 extras 的类型来源:TYPE_CHECKING 导入需要 pyright 可解析(CI 类型检查用;
72
+ # bare-install 冒烟在独立 venv 中进行,不受影响)
73
+ "aiosqlite>=0.19",
74
+ "httpx>=0.27",
75
+ "redis>=5",
76
+ "import-linter>=2.0",
77
+ "pyright>=1.1.380",
78
+ "ruff>=0.6",
79
+ ]
80
+
81
+ [tool.ruff]
82
+ line-length = 100
83
+ target-version = "py310"
84
+
85
+ [tool.ruff.lint]
86
+ # 显式收敛规则集(CI 确定性):正确性(E4/E7/E9/F)+ import 排序(I)。
87
+ # 自愈式代码大量使用 except Exception 兜底并记录日志(BLE/S 系不适用)。
88
+ select = ["E4", "E7", "E9", "F", "I"]
89
+
90
+ [tool.importlinter]
91
+ root_packages = ["streamgate"]
92
+
93
+ [[tool.importlinter.contracts]]
94
+ name = "Layered architecture (higher layers may import lower layers)"
95
+ type = "layers"
96
+ layers = [
97
+ "streamgate.ingest | streamgate.consumer",
98
+ "streamgate.db",
99
+ "streamgate.cache | streamgate.specs | streamgate.transport | streamgate.resilience",
100
+ "streamgate.protocols | streamgate.config | streamgate.obs",
101
+ ]
102
+ # Tier 0 sugar 载体例外:specs 依赖 mssql 方言的 cast 类型表,
103
+ # 与 db→specs 构成容器级环,此处显式豁免该条导入(真实架构见仓库 README)。
104
+ ignore_imports = [
105
+ "streamgate.specs -> streamgate.db.dialects.mssql",
106
+ ]
107
+
108
+ [tool.pyright]
109
+ pythonVersion = "3.10"
110
+ include = ["src/streamgate"]
111
+ typeCheckingMode = "basic"