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.
- streamgate-0.1.0/.gitignore +26 -0
- streamgate-0.1.0/.import_linter_cache/.gitignore +2 -0
- streamgate-0.1.0/.import_linter_cache/5780e4798d429798e55d962d9844c38a9b5e0494.data.json +1 -0
- streamgate-0.1.0/.import_linter_cache/CACHEDIR.TAG +3 -0
- streamgate-0.1.0/.import_linter_cache/streamgate.meta.json +1 -0
- streamgate-0.1.0/AGENTS.md +12 -0
- streamgate-0.1.0/LICENSE +21 -0
- streamgate-0.1.0/PKG-INFO +189 -0
- streamgate-0.1.0/README.md +146 -0
- streamgate-0.1.0/pyproject.toml +111 -0
- streamgate-0.1.0/src/streamgate/__init__.py +214 -0
- streamgate-0.1.0/src/streamgate/_optional.py +38 -0
- streamgate-0.1.0/src/streamgate/cache/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/cache/existence.py +233 -0
- streamgate-0.1.0/src/streamgate/config.py +150 -0
- streamgate-0.1.0/src/streamgate/consumer/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/consumer/classifier.py +139 -0
- streamgate-0.1.0/src/streamgate/consumer/dlq.py +375 -0
- streamgate-0.1.0/src/streamgate/consumer/loop.py +649 -0
- streamgate-0.1.0/src/streamgate/consumer/runner.py +233 -0
- streamgate-0.1.0/src/streamgate/db/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/db/backfill.py +145 -0
- streamgate-0.1.0/src/streamgate/db/dialects/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/db/dialects/mssql.py +126 -0
- streamgate-0.1.0/src/streamgate/db/dialects/sqlite.py +57 -0
- streamgate-0.1.0/src/streamgate/db/engines.py +113 -0
- streamgate-0.1.0/src/streamgate/db/upsert.py +205 -0
- streamgate-0.1.0/src/streamgate/ingest/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/ingest/admission/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/ingest/admission/no_admission.py +37 -0
- streamgate-0.1.0/src/streamgate/ingest/admission/redis_existence.py +489 -0
- streamgate-0.1.0/src/streamgate/ingest/gateway.py +328 -0
- streamgate-0.1.0/src/streamgate/ingest/producer.py +299 -0
- streamgate-0.1.0/src/streamgate/obs/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/obs/logging.py +43 -0
- streamgate-0.1.0/src/streamgate/obs/metrics.py +30 -0
- streamgate-0.1.0/src/streamgate/protocols.py +355 -0
- streamgate-0.1.0/src/streamgate/resilience/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/resilience/backpressure.py +370 -0
- streamgate-0.1.0/src/streamgate/resilience/health.py +266 -0
- streamgate-0.1.0/src/streamgate/specs.py +133 -0
- streamgate-0.1.0/src/streamgate/transport/__init__.py +0 -0
- streamgate-0.1.0/src/streamgate/transport/codec.py +62 -0
- streamgate-0.1.0/src/streamgate/transport/kafka.py +194 -0
- 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 @@
|
|
|
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 @@
|
|
|
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
|
+
|
streamgate-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/streamgate/)
|
|
47
|
+
[](https://pypi.org/project/streamgate/)
|
|
48
|
+
[](https://github.com/{github_owner}/streamgate/blob/main/LICENSE)
|
|
49
|
+
[](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
|
+
[](https://pypi.org/project/streamgate/)
|
|
4
|
+
[](https://pypi.org/project/streamgate/)
|
|
5
|
+
[](https://github.com/{github_owner}/streamgate/blob/main/LICENSE)
|
|
6
|
+
[](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"
|