streamgate 0.2.0__tar.gz → 0.3.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 (87) hide show
  1. {streamgate-0.2.0 → streamgate-0.3.0}/.github/workflows/ci.yml +5 -4
  2. streamgate-0.3.0/.import_linter_cache/a1215eee6d5c7b9f11de3767d6965c4a9c703b3e.data.json +1 -0
  3. streamgate-0.3.0/.import_linter_cache/streamgate.meta.json +1 -0
  4. {streamgate-0.2.0 → streamgate-0.3.0}/CHANGELOG.md +47 -0
  5. {streamgate-0.2.0 → streamgate-0.3.0}/CONFIGURATION.md +51 -5
  6. {streamgate-0.2.0 → streamgate-0.3.0}/CONTRIBUTING.md +3 -2
  7. {streamgate-0.2.0 → streamgate-0.3.0}/PKG-INFO +53 -22
  8. {streamgate-0.2.0 → streamgate-0.3.0}/README.md +43 -21
  9. {streamgate-0.2.0 → streamgate-0.3.0}/README.zh-CN.md +43 -17
  10. {streamgate-0.2.0 → streamgate-0.3.0}/examples/docker-compose.yml +3 -3
  11. {streamgate-0.2.0 → streamgate-0.3.0}/examples/http_probe/README.md +13 -7
  12. {streamgate-0.2.0 → streamgate-0.3.0}/examples/http_probe/produce.py +3 -2
  13. streamgate-0.3.0/examples/prod_pipeline/README.md +66 -0
  14. streamgate-0.3.0/examples/prod_pipeline/consume.py +96 -0
  15. streamgate-0.3.0/examples/prod_pipeline/health_server.py +46 -0
  16. streamgate-0.3.0/examples/prod_pipeline/models.py +29 -0
  17. streamgate-0.3.0/examples/prod_pipeline/produce.py +103 -0
  18. {streamgate-0.2.0 → streamgate-0.3.0}/examples/pure_pipeline/README.md +5 -3
  19. {streamgate-0.2.0 → streamgate-0.3.0}/examples/redis_admission/README.md +16 -13
  20. {streamgate-0.2.0 → streamgate-0.3.0}/examples/redis_admission/produce.py +8 -5
  21. streamgate-0.3.0/examples/sqlite_sink/README.md +53 -0
  22. {streamgate-0.2.0 → streamgate-0.3.0}/examples/sqlite_sink/consume.py +10 -5
  23. {streamgate-0.2.0 → streamgate-0.3.0}/pyproject.toml +17 -24
  24. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/__init__.py +7 -6
  25. streamgate-0.3.0/src/streamgate/contrib/__init__.py +16 -0
  26. streamgate-0.3.0/src/streamgate/contrib/_deps.py +24 -0
  27. streamgate-0.3.0/src/streamgate/contrib/http_probe/__init__.py +19 -0
  28. {streamgate-0.2.0/examples → streamgate-0.3.0/src/streamgate/contrib}/http_probe/probe_signal.py +4 -1
  29. streamgate-0.3.0/src/streamgate/contrib/mssql_sink/__init__.py +30 -0
  30. streamgate-0.3.0/src/streamgate/contrib/redis_admission/__init__.py +39 -0
  31. {streamgate-0.2.0/examples → streamgate-0.3.0/src/streamgate/contrib}/redis_admission/admission.py +3 -3
  32. streamgate-0.2.0/examples/redis_admission/settings.py → streamgate-0.3.0/src/streamgate/contrib/redis_admission/config.py +2 -2
  33. {streamgate-0.2.0/examples → streamgate-0.3.0/src/streamgate/contrib}/redis_admission/existence.py +1 -1
  34. streamgate-0.3.0/src/streamgate/contrib/sql_sink/__init__.py +36 -0
  35. streamgate-0.3.0/src/streamgate/contrib/sql_sink/_dialects/__init__.py +5 -0
  36. {streamgate-0.2.0/examples/sqlite_sink → streamgate-0.3.0/src/streamgate/contrib/sql_sink}/backfill.py +6 -3
  37. {streamgate-0.2.0/examples/sqlite_sink → streamgate-0.3.0/src/streamgate/contrib/sql_sink}/classifier.py +3 -3
  38. {streamgate-0.2.0/examples/sqlite_sink → streamgate-0.3.0/src/streamgate/contrib/sql_sink}/config.py +4 -4
  39. {streamgate-0.2.0/examples/sqlite_sink → streamgate-0.3.0/src/streamgate/contrib/sql_sink}/engines.py +2 -2
  40. {streamgate-0.2.0/examples/sqlite_sink → streamgate-0.3.0/src/streamgate/contrib/sql_sink}/upsert.py +9 -8
  41. streamgate-0.3.0/src/streamgate/contrib/sqlite_sink/__init__.py +29 -0
  42. {streamgate-0.2.0 → streamgate-0.3.0}/uv.lock +23 -12
  43. streamgate-0.2.0/.import_linter_cache/a1215eee6d5c7b9f11de3767d6965c4a9c703b3e.data.json +0 -1
  44. streamgate-0.2.0/.import_linter_cache/streamgate.meta.json +0 -1
  45. streamgate-0.2.0/examples/sqlite_sink/README.md +0 -48
  46. streamgate-0.2.0/examples/sqlite_sink/dialects/__init__.py +0 -1
  47. {streamgate-0.2.0 → streamgate-0.3.0}/.github/workflows/release.yml +0 -0
  48. {streamgate-0.2.0 → streamgate-0.3.0}/.gitignore +0 -0
  49. {streamgate-0.2.0 → streamgate-0.3.0}/.import_linter_cache/.gitignore +0 -0
  50. {streamgate-0.2.0 → streamgate-0.3.0}/.import_linter_cache/5780e4798d429798e55d962d9844c38a9b5e0494.data.json +0 -0
  51. {streamgate-0.2.0 → streamgate-0.3.0}/.import_linter_cache/CACHEDIR.TAG +0 -0
  52. {streamgate-0.2.0 → streamgate-0.3.0}/AGENTS.md +0 -0
  53. {streamgate-0.2.0 → streamgate-0.3.0}/LICENSE +0 -0
  54. {streamgate-0.2.0 → streamgate-0.3.0}/examples/http_probe/consume.py +0 -0
  55. {streamgate-0.2.0 → streamgate-0.3.0}/examples/http_probe/health_server.py +0 -0
  56. {streamgate-0.2.0 → streamgate-0.3.0}/examples/http_probe/models.py +0 -0
  57. {streamgate-0.2.0 → streamgate-0.3.0}/examples/pure_pipeline/consume.py +0 -0
  58. {streamgate-0.2.0 → streamgate-0.3.0}/examples/pure_pipeline/models.py +0 -0
  59. {streamgate-0.2.0 → streamgate-0.3.0}/examples/pure_pipeline/produce.py +0 -0
  60. {streamgate-0.2.0 → streamgate-0.3.0}/examples/redis_admission/models.py +0 -0
  61. {streamgate-0.2.0 → streamgate-0.3.0}/examples/sqlite_sink/models.py +0 -0
  62. {streamgate-0.2.0 → streamgate-0.3.0}/examples/sqlite_sink/produce.py +0 -0
  63. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/config.py +0 -0
  64. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/consumer/__init__.py +0 -0
  65. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/consumer/classifier.py +0 -0
  66. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/consumer/dlq.py +0 -0
  67. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/consumer/loop.py +0 -0
  68. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/consumer/runner.py +0 -0
  69. {streamgate-0.2.0/examples/sqlite_sink/dialects → streamgate-0.3.0/src/streamgate/contrib/sql_sink/_dialects}/mssql.py +0 -0
  70. {streamgate-0.2.0/examples/sqlite_sink/dialects → streamgate-0.3.0/src/streamgate/contrib/sql_sink/_dialects}/sqlite.py +0 -0
  71. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/ingest/__init__.py +0 -0
  72. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/ingest/admission/__init__.py +0 -0
  73. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/ingest/admission/in_memory.py +0 -0
  74. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/ingest/admission/no_admission.py +0 -0
  75. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/ingest/gateway.py +0 -0
  76. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/ingest/producer.py +0 -0
  77. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/obs/__init__.py +0 -0
  78. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/obs/logging.py +0 -0
  79. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/obs/metrics.py +0 -0
  80. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/protocols.py +0 -0
  81. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/resilience/__init__.py +0 -0
  82. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/resilience/backpressure.py +0 -0
  83. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/resilience/health.py +0 -0
  84. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/specs.py +0 -0
  85. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/transport/__init__.py +0 -0
  86. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/transport/codec.py +0 -0
  87. {streamgate-0.2.0 → streamgate-0.3.0}/src/streamgate/transport/kafka.py +0 -0
@@ -22,7 +22,7 @@ jobs:
22
22
  run: uv sync
23
23
  - name: Lint
24
24
  run: uv run ruff check .
25
- - name: Import contracts (layering + pure-core forbidden modules)
25
+ - name: Import contracts (layered architecture; core never imports contrib)
26
26
  run: uv run lint-imports
27
27
  - name: Type check
28
28
  run: uv run pyright
@@ -31,8 +31,9 @@ jobs:
31
31
 
32
32
  examples:
33
33
  # Examples are the storefront: they must meet the same quality bar as the
34
- # package (lint + typecheck). Dev group carries the third-party libs the
35
- # example modules import (sqlalchemy/sqlmodel/redis/httpx/aioodbc).
34
+ # package (lint + typecheck). The contrib strategies the examples import
35
+ # carry their third-party libs via extras ([redis]/[sql]/[http]), pulled in
36
+ # by the dev group's self-referential dependency.
36
37
  name: examples (lint + typecheck)
37
38
  runs-on: ubuntu-latest
38
39
  steps:
@@ -77,7 +78,7 @@ jobs:
77
78
  steps:
78
79
  - uses: actions/checkout@v4
79
80
  - uses: astral-sh/setup-uv@v5
80
- - name: Bare install (no extras — the extras concept no longer exists)
81
+ - name: Bare install (no extras — contrib and its dependencies are opt-in)
81
82
  run: |
82
83
  uv venv bare
83
84
  uv pip install --python bare/bin/python .
@@ -0,0 +1 @@
1
+ {"imports_by_module":{"streamgate":[["streamgate.obs.metrics",false,42,"from streamgate.obs.metrics import LoggingMetricsSink, MetricsSink"],["streamgate.consumer.classifier",false,26,"from streamgate.consumer.classifier import DefaultErrorClassifier"],["streamgate.consumer.runner",false,36,"from streamgate.consumer.runner import ConsumerWorker"],["streamgate.protocols",false,43,"from streamgate.protocols import ("],["streamgate.ingest.gateway",false,39,"from streamgate.ingest.gateway import IngestGateway"],["streamgate.config",false,25,"from streamgate.config import BackpressureConfig, ConsumerConfig, KafkaConfig"],["streamgate.ingest.admission.in_memory",false,37,"from streamgate.ingest.admission.in_memory import InMemoryAdmission"],["streamgate.ingest.producer",false,40,"from streamgate.ingest.producer import KafkaProducerService"],["streamgate.consumer.dlq",false,27,"from streamgate.consumer.dlq import ("],["streamgate.resilience.backpressure",false,66,"from streamgate.resilience.backpressure import ManualBackpressureSignal"],["streamgate.ingest.admission.no_admission",false,38,"from streamgate.ingest.admission.no_admission import NoAdmission"],["streamgate.resilience.health",false,67,"from streamgate.resilience.health import ("],["streamgate.consumer.loop",false,35,"from streamgate.consumer.loop import ConsumeRuntime, consume_loop"],["streamgate.transport.kafka",false,75,"from streamgate.transport.kafka import KafkaConsumerService"],["streamgate.specs",false,73,"from streamgate.specs import ConsumeSpec, IngestBinding, IngestRecordT"],["streamgate.transport.codec",false,74,"from streamgate.transport.codec import JsonEnvelopeCodec"],["streamgate.obs.logging",false,41,"from streamgate.obs.logging import configure_logger, logger"]],"streamgate.config":[["pydantic",false,8,"from pydantic import BaseModel, field_validator"]],"streamgate.consumer":[],"streamgate.consumer.classifier":[["streamgate.protocols",false,20,"from streamgate.protocols import ErrorKind"],["asyncio",false,16,"import asyncio"],["aiokafka",false,18,"from aiokafka.errors import KafkaError"]],"streamgate.consumer.dlq":[["asyncio",false,15,"import asyncio"],["json",false,16,"import json"],["datetime",false,18,"from datetime import datetime, timezone"],["aiokafka",false,20,"from aiokafka import AIOKafkaProducer"],["streamgate.config",false,22,"from streamgate.config import KafkaConfig"],["streamgate.obs.logging",false,23,"from streamgate.obs.logging import logger"],["streamgate.protocols",false,24,"from streamgate.protocols import ErrorKind, JsonObject, RecordWriter"],["dataclasses",false,17,"from dataclasses import dataclass, field"]],"streamgate.consumer.loop":[["asyncio",false,5,"import asyncio"],["collections",false,7,"from collections.abc import Callable"],["streamgate.transport.kafka",false,37,"from streamgate.transport.kafka import KafkaConsumerService, KafkaRecord"],["streamgate.protocols",false,26,"from streamgate.protocols import ("],["aiokafka",false,11,"from aiokafka.structs import OffsetAndMetadata, TopicPartition"],["streamgate.obs.logging",false,24,"from streamgate.obs.logging import logger"],["streamgate.consumer.classifier",false,14,"from streamgate.consumer.classifier import DefaultErrorClassifier"],["datetime",false,8,"from datetime import datetime, timezone"],["streamgate.config",false,13,"from streamgate.config import ConsumerConfig"],["streamgate.obs.metrics",false,25,"from streamgate.obs.metrics import DEFAULT_METRICS, MetricsSink"],["streamgate.specs",false,36,"from streamgate.specs import ConsumeSpec"],["streamgate.consumer.dlq",false,15,"from streamgate.consumer.dlq import ("],["aiokafka",false,10,"from aiokafka.errors import CommitFailedError"],["time",false,6,"import time"]],"streamgate.consumer.runner":[["streamgate.resilience.health",false,16,"from streamgate.resilience.health import ("],["signal",false,9,"import signal"],["streamgate.protocols",false,15,"from streamgate.protocols import ErrorClassifier, MessageCodec"],["asyncio",false,8,"import asyncio"],["streamgate.consumer.loop",false,13,"from streamgate.consumer.loop import ConsumeRuntime, consume_loop"],["streamgate.specs",false,20,"from streamgate.specs import ConsumeSpec"],["streamgate.obs.logging",false,14,"from streamgate.obs.logging import logger"],["streamgate.transport.kafka",false,22,"from streamgate.transport.kafka import KafkaConsumerService"],["streamgate.config",false,11,"from streamgate.config import ConsumerConfig, KafkaConfig"],["streamgate.consumer.dlq",false,12,"from streamgate.consumer.dlq import DlqProducer"],["streamgate.transport.codec",false,21,"from streamgate.transport.codec import JsonEnvelopeCodec"]],"streamgate.contrib":[],"streamgate.contrib._deps":[["typing",false,3,"from typing import NoReturn"]],"streamgate.contrib.http_probe":[["streamgate.contrib._deps",false,9,"from streamgate.contrib._deps import require_extra_import"],["streamgate.contrib.http_probe.probe_signal",false,12,"from streamgate.contrib.http_probe.probe_signal import ("]],"streamgate.contrib.http_probe.probe_signal":[["streamgate.protocols",false,25,"from streamgate.protocols import BackpressureSnapshot, ProbeResult"],["httpx",false,22,"import httpx"],["streamgate",false,24,"from streamgate import BackpressureConfig, logger"],["time",false,20,"import time"],["asyncio",false,19,"import asyncio"]],"streamgate.contrib.mssql_sink":[["streamgate.contrib.sql_sink",false,10,"from streamgate.contrib.sql_sink import ("]],"streamgate.contrib.redis_admission":[["streamgate.contrib.redis_admission.admission",false,11,"from streamgate.contrib.redis_admission.admission import ("],["streamgate.contrib._deps",false,8,"from streamgate.contrib._deps import require_extra_import"],["streamgate.contrib.redis_admission.config",false,20,"from streamgate.contrib.redis_admission.config import RedisConfig"],["streamgate.contrib.redis_admission.existence",false,21,"from streamgate.contrib.redis_admission.existence import ("]],"streamgate.contrib.redis_admission.admission":[["streamgate.contrib.redis_admission.config",false,19,"from streamgate.contrib.redis_admission.config import RedisConfig"],["dataclasses",false,12,"from dataclasses import dataclass"],["enum",false,13,"from enum import Enum"],["redis",false,16,"from redis.exceptions import RedisError"],["streamgate",false,18,"from streamgate import logger"],["collections",false,11,"from collections.abc import Callable"],["asyncio",false,10,"import asyncio"],["streamgate.protocols",false,21,"from streamgate.protocols import ("],["typing",false,14,"from typing import Generic"],["streamgate.contrib.redis_admission.existence",false,20,"from streamgate.contrib.redis_admission.existence import RedisExistenceCache"]],"streamgate.contrib.redis_admission.config":[["pydantic",false,6,"from pydantic import BaseModel"]],"streamgate.contrib.redis_admission.existence":[["redis",false,14,"import redis.asyncio as aioredis"],["typing",false,12,"from typing import TypeVar"],["streamgate",false,16,"from streamgate import logger"],["json",false,10,"import json"],["streamgate.contrib.redis_admission.config",false,17,"from streamgate.contrib.redis_admission.config import RedisConfig"],["collections",false,11,"from collections.abc import Awaitable"],["asyncio",false,9,"import asyncio"]],"streamgate.contrib.sql_sink":[["streamgate.contrib.sql_sink.engines",false,15,"from streamgate.contrib.sql_sink.engines import ("],["streamgate.contrib.sql_sink.classifier",false,13,"from streamgate.contrib.sql_sink.classifier import SQLAlchemyErrorClassifier"],["streamgate.contrib.sql_sink.config",false,14,"from streamgate.contrib.sql_sink.config import DbConfig"],["streamgate.contrib.sql_sink.upsert",false,19,"from streamgate.contrib.sql_sink.upsert import ("],["streamgate.contrib.sql_sink.backfill",false,12,"from streamgate.contrib.sql_sink.backfill import SqlBackfill"],["streamgate.contrib._deps",false,9,"from streamgate.contrib._deps import require_extra_import"]],"streamgate.contrib.sql_sink._dialects":[],"streamgate.contrib.sql_sink._dialects.mssql":[["sqlmodel",false,19,"from sqlmodel import SQLModel"],["sqlalchemy",false,18,"from sqlalchemy.dialects import mssql"],["collections",false,14,"from collections.abc import Iterator"],["streamgate.protocols",false,21,"from streamgate.protocols import JsonObject"],["sqlalchemy",false,16,"from sqlalchemy import String, TextClause, text"],["sqlalchemy",false,17,"from sqlalchemy import inspect as sa_inspect"]],"streamgate.contrib.sql_sink._dialects.sqlite":[["sqlalchemy",false,4,"from sqlalchemy.dialects.sqlite import insert as sqlite_insert"],["sqlmodel",false,6,"from sqlmodel import SQLModel"],["sqlalchemy",false,5,"from sqlalchemy.ext.asyncio import AsyncSession"],["sqlalchemy",false,3,"from sqlalchemy import text"],["streamgate.protocols",false,8,"from streamgate.protocols import JsonObject"]],"streamgate.contrib.sql_sink.backfill":[["streamgate",false,13,"from streamgate import logger"],["sqlalchemy",false,11,"from sqlalchemy.ext.asyncio import AsyncEngine"],["datetime",false,8,"from datetime import datetime"],["sqlalchemy",false,10,"from sqlalchemy import text"],["asyncio",false,7,"import asyncio"],["streamgate.protocols",false,14,"from streamgate.protocols import JsonObject"]],"streamgate.contrib.sql_sink.classifier":[["sqlalchemy",false,23,"from sqlalchemy.exc import ("],["sqlalchemy",false,28,"from sqlalchemy.exc import ("],["streamgate",false,32,"from streamgate import ErrorKind"],["collections",false,20,"from collections.abc import Iterator"],["asyncio",false,18,"import asyncio"],["re",false,19,"import re"],["aiokafka",false,22,"from aiokafka.errors import KafkaError"]],"streamgate.contrib.sql_sink.config":[["pydantic",false,7,"from pydantic import BaseModel"]],"streamgate.contrib.sql_sink.engines":[["sqlalchemy",false,3,"from sqlalchemy import event"],["sqlalchemy",false,4,"from sqlalchemy.ext.asyncio import ("],["streamgate.contrib.sql_sink.config",false,12,"from streamgate.contrib.sql_sink.config import DbConfig"],["streamgate",false,11,"from streamgate import logger"]],"streamgate.contrib.sql_sink.upsert":[["streamgate.contrib.sql_sink._dialects.mssql",false,19,"from streamgate.contrib.sql_sink._dialects.mssql import mssql_cast_types"],["streamgate.protocols",false,21,"from streamgate.protocols import JsonObject, WriteResult"],["collections",false,9,"from collections.abc import Callable"],["asyncio",false,8,"import asyncio"],["streamgate.contrib.sql_sink.config",false,20,"from streamgate.contrib.sql_sink.config import DbConfig"],["sqlmodel",false,14,"from sqlmodel import SQLModel"],["sqlalchemy",false,11,"from sqlalchemy import inspect as sa_inspect"],["streamgate.contrib.sql_sink._dialects.mssql",false,17,"from streamgate.contrib.sql_sink._dialects import mssql as mssql_dialect"],["sqlalchemy",false,13,"from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker"],["streamgate",false,16,"from streamgate import logger"],["sqlalchemy",false,12,"from sqlalchemy import text"],["streamgate.contrib.sql_sink._dialects.sqlite",false,18,"from streamgate.contrib.sql_sink._dialects import sqlite as sqlite_dialect"]],"streamgate.contrib.sqlite_sink":[["streamgate.contrib.sql_sink",false,9,"from streamgate.contrib.sql_sink import ("]],"streamgate.ingest":[],"streamgate.ingest.admission":[],"streamgate.ingest.admission.in_memory":[["collections",false,12,"from collections.abc import Callable"],["typing",false,13,"from typing import Generic"],["streamgate.protocols",false,15,"from streamgate.protocols import Decision, JsonObject, RecordT"]],"streamgate.ingest.admission.no_admission":[["typing",false,3,"from typing import Generic"],["streamgate.protocols",false,5,"from streamgate.protocols import Decision, RecordT"]],"streamgate.ingest.gateway":[["streamgate.protocols",false,16,"from streamgate.protocols import ("],["streamgate.obs.logging",false,15,"from streamgate.obs.logging import logger"],["typing",false,9,"from typing import Generic"],["streamgate.config",false,11,"from streamgate.config import BackpressureConfig, KafkaConfig"],["streamgate.ingest.producer",false,14,"from streamgate.ingest.producer import KafkaProducerService"],["streamgate.resilience.health",false,26,"from streamgate.resilience.health import ("],["streamgate.transport.codec",false,31,"from streamgate.transport.codec import JsonEnvelopeCodec"],["streamgate.ingest.admission.in_memory",false,12,"from streamgate.ingest.admission.in_memory import InMemoryAdmission"],["streamgate.specs",false,30,"from streamgate.specs import IngestBinding, IngestRecordT"],["streamgate.ingest.admission.no_admission",false,13,"from streamgate.ingest.admission.no_admission import NoAdmission"],["datetime",false,8,"from datetime import datetime, timezone"],["streamgate.resilience.backpressure",false,25,"from streamgate.resilience.backpressure import ManualBackpressureSignal"]],"streamgate.ingest.producer":[["json",false,9,"import json"],["streamgate.obs.logging",false,17,"from streamgate.obs.logging import logger"],["aiokafka",false,13,"from aiokafka import AIOKafkaProducer"],["aiokafka",false,14,"from aiokafka.errors import KafkaConnectionError, KafkaError"],["asyncio",false,8,"import asyncio"],["time",false,10,"import time"],["datetime",false,11,"from datetime import datetime, timezone"],["streamgate.protocols",false,18,"from streamgate.protocols import JsonObject"],["streamgate.config",false,16,"from streamgate.config import KafkaConfig"]],"streamgate.obs":[],"streamgate.obs.logging":[["loguru",false,6,"from loguru._handler import Message"],["datetime",false,3,"from datetime import timezone"],["loguru",false,5,"from loguru import logger as _logger"],["sys",false,2,"import sys"],["json",false,1,"import json"]],"streamgate.obs.metrics":[["streamgate.obs.logging",false,10,"from streamgate.obs.logging import logger"],["typing",false,8,"from typing import Protocol"]],"streamgate.protocols":[["typing",false,11,"from typing import Protocol, TypeVar, runtime_checkable"],["collections",false,7,"from collections.abc import Awaitable, Callable"],["datetime",false,9,"from datetime import datetime"],["enum",false,10,"from enum import Enum"],["dataclasses",false,8,"from dataclasses import dataclass, field"]],"streamgate.resilience":[],"streamgate.resilience.backpressure":[["streamgate.protocols",false,10,"from streamgate.protocols import AllowAllSignal, BackpressureSnapshot"]],"streamgate.resilience.health":[["datetime",false,10,"from datetime import datetime, timezone"],["asyncio",false,7,"import asyncio"],["typing",false,11,"from typing import Protocol"],["pydantic",false,13,"from pydantic import BaseModel, Field"],["collections",false,9,"from collections.abc import Awaitable"],["streamgate.obs.logging",false,15,"from streamgate.obs.logging import logger"],["time",false,8,"import time"]],"streamgate.specs":[["collections",false,10,"from collections.abc import Callable, Hashable"],["streamgate.protocols",false,16,"from streamgate.protocols import ("],["pydantic",false,14,"from pydantic import BaseModel"],["typing",false,12,"from typing import Generic, Literal, TypeVar"],["dataclasses",false,11,"from dataclasses import dataclass, field"]],"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"],["json",false,8,"import json"]],"streamgate.transport.kafka":[["asyncio",false,3,"import asyncio"],["aiokafka",false,7,"from aiokafka.errors import CommitFailedError, KafkaConnectionError, KafkaError"],["aiokafka",false,5,"from aiokafka import AIOKafkaConsumer"],["aiokafka",false,8,"from aiokafka.structs import ConsumerRecord, OffsetAndMetadata, TopicPartition"],["streamgate.config",false,10,"from streamgate.config import ConsumerConfig, KafkaConfig"],["aiokafka",false,6,"from aiokafka.consumer.group_coordinator import GroupCoordinator"],["streamgate.obs.logging",false,11,"from streamgate.obs.logging import logger"]]},"version":2}
@@ -0,0 +1 @@
1
+ {"streamgate.contrib.sql_sink.backfill": 1788835942.310305, "streamgate.consumer.classifier": 1788778605.4753034, "streamgate.obs.logging": 1788749262.353655, "streamgate.obs.metrics": 1788749264.9746833, "streamgate.contrib.redis_admission.config": 1788835837.2772365, "streamgate": 1788836110.1479475, "streamgate.contrib.redis_admission": 1788835898.0475388, "streamgate.contrib._deps": 1788835802.9044309, "streamgate.consumer": 1788742270.5761602, "streamgate.contrib.sql_sink.config": 1788835924.6027458, "streamgate.ingest.admission.no_admission": 1788759910.7492158, "streamgate.contrib.sql_sink.engines": 1788835942.286183, "streamgate.contrib.http_probe": 1788836174.9308388, "streamgate.consumer.runner": 1788778819.43872, "streamgate.consumer.dlq": 1788778795.1676013, "streamgate.contrib.sql_sink._dialects.sqlite": 1788835977.7374504, "streamgate.config": 1788778673.809221, "streamgate.contrib.sqlite_sink": 1788836174.9292364, "streamgate.contrib": 1788835802.905441, "streamgate.resilience.health": 1788767769.120218, "streamgate.transport": 1788742270.5761602, "streamgate.contrib.sql_sink._dialects": 1788835977.7374504, "streamgate.ingest": 1788742270.5761602, "streamgate.contrib.http_probe.probe_signal": 1788836090.470792, "streamgate.contrib.redis_admission.admission": 1788835898.0445402, "streamgate.contrib.redis_admission.existence": 1788835837.2841742, "streamgate.protocols": 1788780004.1189399, "streamgate.resilience": 1788742270.5761602, "streamgate.transport.kafka": 1788776206.140225, "streamgate.specs": 1788778652.1194282, "streamgate.ingest.admission.in_memory": 1788778617.9172873, "streamgate.ingest.producer": 1788776206.139223, "streamgate.consumer.loop": 1788778765.328234, "streamgate.ingest.gateway": 1788778851.3711455, "streamgate.contrib.mssql_sink": 1788836174.9297483, "streamgate.contrib.sql_sink": 1788836043.336221, "streamgate.contrib.sql_sink._dialects.mssql": 1788836025.5239024, "streamgate.obs": 1788742270.5761602, "streamgate.contrib.sql_sink.classifier": 1788835977.7154298, "streamgate.ingest.admission": 1788742270.5761602, "streamgate.contrib.sql_sink.upsert": 1788836025.5259016, "streamgate.resilience.backpressure": 1788778626.931601, "streamgate.transport.codec": 1788749619.0487723}
@@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2026-09-08
9
+
10
+ **Contrib release.** The production-grade I/O strategies that 0.2.0 demoted to
11
+ copy-paste examples are back in the wheel — as `streamgate.contrib.*`,
12
+ importable, opt-in subpackages. The core stays pure: it gains no dependencies,
13
+ and it still never imports contrib (the import-linter forbidden contract is
14
+ replaced by a layered contract with the same guarantee). Purely additive; no
15
+ breaking changes.
16
+
17
+ ### Added
18
+
19
+ - **`streamgate.contrib`** — official strategy layer (provisional / beta
20
+ grade; ships in the wheel, third-party deps via extras, friendly
21
+ "install streamgate[...]" errors when an extra is missing):
22
+ - `contrib.redis_admission` (extra `[redis]`): `RedisExistenceAdmission`,
23
+ `RedisExistenceCache`, `RedisConfig` — shared-storage uniqueness
24
+ admission with atomic Lua reservation, TTL idle-GC, empty-entity
25
+ sentinels, fail-closed semantics.
26
+ - `contrib.sql_sink` / `contrib.sqlite_sink` / `contrib.mssql_sink`
27
+ (extra `[sql]`): `Upsert`/`UpsertWriter` (`RecordWriter` impl),
28
+ engine factory, `SqlBackfill` (`BackfillSource` impl),
29
+ `SQLAlchemyErrorClassifier`, `DbConfig` — SQLite `ON CONFLICT` and
30
+ MSSQL multi-row `MERGE` + HOLDLOCK dialects.
31
+ - `contrib.http_probe` (extra `[http]`): `HttpProbeSignal` /
32
+ `HysteresisController` — backlog-age trip/recover hysteresis,
33
+ fail-closed probing, kafka-down fast reject.
34
+ - **Extras** `[redis]` / `[sql]` / `[http]` (new names; the pre-0.2.0 extras
35
+ `[sqlite]` / `[mssql]` / `[http-probe]` / `[all]` are not restored).
36
+ - **`examples/prod_pipeline/`** — production-topology demo wiring the three
37
+ contrib components together (Redis uniqueness admission + HTTP-probe
38
+ backpressure + MSSQL idempotent sink; runs on SQLite locally via
39
+ connection-string dialect selection).
40
+
41
+ ### Changed
42
+
43
+ - **import-linter**: the "pure core" forbidden-modules contract is replaced
44
+ by a layered contract — `streamgate.contrib` sits on top and may use the
45
+ core; no core layer may import contrib. Bare-install purity is still
46
+ CI-gated (bare install pulls exactly `aiokafka`/`loguru`/`pydantic`).
47
+ - **`examples/`**: the three strategy examples now import from
48
+ `streamgate.contrib`; demo scripts (produce/consume/health_server/models)
49
+ stay in the repo only. `pure_pipeline/` remains the zero-dependency main
50
+ example and is unchanged.
51
+ - **Dev group** now depends on `streamgate[redis,sql,http]` instead of
52
+ duplicating the third-party libs, keeping a single source of truth for
53
+ contrib dependency versions.
54
+
8
55
  ## [0.2.0] - 2026-09-07
9
56
 
10
57
  **Pure-core release.** All database, Redis and HTTP-client code is removed from
@@ -14,10 +14,56 @@ underscore):
14
14
  is missing, assembly fails at startup with an error that names the setting and
15
15
  how to fix it — the process never runs with project-specific guessed defaults.
16
16
 
17
- Database and Redis connection settings are **not part of the framework**:
17
+ Database and Redis connection settings are **not part of the core framework**:
18
18
  storage carriers are injected via protocols and their configuration belongs to
19
- your application (see the example-local configs in `examples/sqlite_sink/` and
20
- `examples/redis_admission/`).
19
+ your application. The official strategy implementations under
20
+ `streamgate.contrib` ship typed config objects (below) that you construct and
21
+ pass to the strategy classes.
22
+
23
+ ---
24
+
25
+ ## Contrib strategy configs
26
+
27
+ These are plain constructor arguments (not env-mapped). All belong to
28
+ `streamgate.contrib` packages — install the matching extra first
29
+ (`[redis]` / `[sql]` / `[http]`).
30
+
31
+ ### `contrib.redis_admission.RedisConfig`
32
+
33
+ | Field | Default | Description |
34
+ |-------|---------|-------------|
35
+ | `url` | `redis://localhost:6379/0` | Redis connection URL. |
36
+ | `key_prefix` | `streamgate:` | Key prefix for existence hashes. |
37
+ | `existence_ttl_seconds` | `18000` | Existence hash TTL (idle-GC heartbeat: every write renews it). |
38
+ | `empty_existence_ttl_seconds` | `3600` | Empty-entity sentinel TTL (shorter than existence TTL). |
39
+ | `socket_timeout_ms` | `1000` | Read/query path timeout. |
40
+ | `recv_timeout_ms` | `500` | Ingest path (reserve/summary write) timeout. |
41
+ | `fail_closed_on_unavailable` | `true` | Reject instead of silently degrading when Redis is unavailable (`false` = legacy degraded escape hatch). |
42
+
43
+ Strategy-level knobs live on `RedisExistenceAdmissionConfig`
44
+ (`fail_closed_on_unavailable`, `cold_path_max_concurrency`, gate-full /
45
+ unavailable `retry_after` seconds, stable error codes, `log_context` mapping).
46
+
47
+ ### `contrib.sql_sink.DbConfig`
48
+
49
+ | Field | Default | Description |
50
+ |-------|---------|-------------|
51
+ | `connection_string` | **required** | SQLAlchemy async URL (`sqlite+aiosqlite:///...` or `mssql+aioodbc://...`). Missing ⇒ assembly error with fix instructions. |
52
+ | `echo` | `false` | SQLAlchemy echo logging. |
53
+ | `write_timeout_seconds` | `20` | Driver statement timeout (MSSQL/pyodbc hook; must be < `write_wait_seconds`). |
54
+ | `write_wait_seconds` | `25` | `UpsertWriter.write` call-level `wait_for` ceiling. |
55
+ | `pool_timeout_seconds` | `3` | Pool checkout timeout (fail fast when exhausted). |
56
+ | `write_pool_size` | `10` | Fixed write-pool connections. |
57
+ | `write_pool_max_overflow` | `20` | Write-pool overflow connections. |
58
+
59
+ The dialect (SQLite vs MSSQL) is selected by the connection string;
60
+ `contrib.sqlite_sink` and `contrib.mssql_sink` are thin entry packages
61
+ documenting each backend.
62
+
63
+ ### Backpressure probing (contrib.http_probe)
64
+
65
+ `HttpProbeSignal` consumes the same `BACKPRESSURE__*` environment mapping via
66
+ `BackpressureConfig` (see below) — no extra configuration surface.
21
67
 
22
68
  ---
23
69
 
@@ -60,8 +106,8 @@ your application (see the example-local configs in `examples/sqlite_sink/` and
60
106
  ## BackpressureConfig (`BACKPRESSURE__*`)
61
107
 
62
108
  The gateway defaults to the built-in `ManualBackpressureSignal` (static
63
- switch). For dynamic probing inject your own `BackpressureSignal` — the
64
- HTTP-probe reference in `examples/http_probe/` reads this config object.
109
+ switch). For dynamic probing inject your own `BackpressureSignal` —
110
+ `contrib.http_probe.HttpProbeSignal` reads this config object.
65
111
 
66
112
  | Env var | Default | Description |
67
113
  |---------|---------|-------------|
@@ -26,8 +26,9 @@ uv build # wheel + sdist build
26
26
  ## Scope and conventions
27
27
 
28
28
  - **Mechanisms vs. policies.** The package owns mechanisms (admission orchestration, delivery, backpressure, self-healing). Policy choices (entity/slot semantics, schemas, storage carriers) belong to users. PRs should keep this boundary.
29
- - **Pure-core gate.** The package must never import `sqlalchemy`, `sqlmodel`, `redis`, `aiosqlite`, `aioodbc` or `httpx` — enforced by the import-linter forbidden contract in `pyproject.toml` (`[tool.importlinter]`). I/O strategy implementations belong in `examples/` as copy-paste code. If you add a protocol extension point, add or update a runnable example for it in the same PR.
30
- - **Examples are the storefront.** Code in `examples/` is held to the same quality bar as the package itself: CI runs lint and typecheck over it, and example READMEs must match reality. Rotting examples rot the project.
29
+ - **Core purity / layering gate.** The core layers must never import `streamgate.contrib`, and `import streamgate` must work with zero I/O third-party dependencies — enforced by the import-linter layered contract in `pyproject.toml` (`[tool.importlinter]`) plus the CI bare-install smoke. Official I/O strategy implementations belong in `streamgate.contrib.*` with their third-party dependencies declared as extras (`[redis]` / `[sql]` / `[http]`); a missing extra must raise an import error that names the extra (use `streamgate.contrib._deps.require_extra_import`). If you add a protocol extension point, add or update a contrib implementation and a runnable example for it in the same PR.
30
+ - **Contrib is provisional.** Contrib packages are public API at beta grade: signatures may adjust within minor releases, but behavior changes and deprecations must go through the CHANGELOG. Merged strategy code must keep behavior-preserving moves only (lint + pyright gates apply to it exactly like the core).
31
+ - **Examples are the storefront.** Code in `examples/` is held to the same quality bar as the package itself: CI runs lint and typecheck over it, and example READMEs must match reality. Demos import strategies from `streamgate.contrib` — never copy strategy source into `examples/`. Rotting examples rot the project.
31
32
  - **Architecture gate.** The layering contract in `pyproject.toml` (`[tool.importlinter]`) is enforced: higher layers may import lower layers, never the reverse. If your change legitimately needs a new edge, update the contract in the same PR and explain why.
32
33
  - **Comments language.** Internal docstrings/comments are written in Chinese and kept that way. Docstrings on public API symbols (exported from `streamgate.__init__`) should be English-friendly.
33
34
  - **No test suite yet.** The project currently ships without tests (top roadmap item). Until then, please include a minimal reproduction script (or a runnable snippet) with bug reports and behavioral PRs so reviewers can verify against real data.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: streamgate
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A pure-core Kafka data pipeline framework: conditional admission, reliable delivery, pluggable sinks via typed hooks.
5
5
  Project-URL: Homepage, https://github.com/pwg-code/streamgate
6
6
  Project-URL: Repository, https://github.com/pwg-code/streamgate
@@ -23,6 +23,15 @@ Requires-Python: >=3.10
23
23
  Requires-Dist: aiokafka>=0.10
24
24
  Requires-Dist: loguru>=0.7
25
25
  Requires-Dist: pydantic>=2.5
26
+ Provides-Extra: http
27
+ Requires-Dist: httpx>=0.27; extra == 'http'
28
+ Provides-Extra: redis
29
+ Requires-Dist: redis>=5; extra == 'redis'
30
+ Provides-Extra: sql
31
+ Requires-Dist: aioodbc>=0.5; extra == 'sql'
32
+ Requires-Dist: aiosqlite>=0.19; extra == 'sql'
33
+ Requires-Dist: sqlalchemy>=2.0; extra == 'sql'
34
+ Requires-Dist: sqlmodel>=0.0.16; extra == 'sql'
26
35
  Description-Content-Type: text/markdown
27
36
 
28
37
  # streamgate
@@ -36,17 +45,17 @@ English | [中文](README.zh-CN.md)
36
45
 
37
46
  **streamgate** is a pure-core Kafka data pipeline framework: conditional admission at the door, reliable delivery through Kafka, and a single typed hook — `RecordWriter` — wherever you want the data to land.
38
47
 
39
- The wheel installs exactly three dependencies (`aiokafka`, `loguru`, `pydantic`) and contains **zero database, Redis or HTTP-client code**. All I/O strategies ship as copy-paste examples in [`examples/`](examples/).
48
+ The wheel installs exactly three dependencies (`aiokafka`, `loguru`, `pydantic`) and the core contains **zero database, Redis or HTTP-client code**. Official I/O strategy implementations live in [`streamgate.contrib`](#contrib-official-strategy-implementations) — opt-in via extras — and runnable demos live in [`examples/`](examples/).
40
49
 
41
50
  - 中文提示:本仓库对外文档以英文为主;内部代码注释保留中文。
42
51
 
43
52
  ## Why streamgate
44
53
 
45
54
  - **Mechanisms in the framework, policies in your hands.** The framework owns admission *orchestration*, backpressure, health probing, reconnection, offset handling, batch buffering and graceful shutdown. You own entity/slot semantics, schemas, and — critically — **the storage carriers**: your database, your Redis, your HTTP probe are injected via typed protocols, never imported by the core.
46
- - **A uniqueness contract without a mandated store.** The admission flow (existence check → atomic slot reservation → cold-entity backfill → consumer-side authoritative refresh) is framework machinery. Whether the existence store is Redis, an in-process dict, or something else is your policy: implement `AdmissionPolicy`, or use the built-in zero-I/O shortcuts (`"none"`, `"in-memory"`). A production-grade Redis reference lives in [`examples/redis_admission/`](examples/redis_admission/).
47
- - **One sink hook, any storage.** `ConsumeSpec(sink=...)` takes a `RecordWriter` — write to SQL, Elasticsearch, another service, a file; the framework handles batching, retries, error classification and DLQ bisection around it. No database is built in, on purpose.
48
- - **Error classification as a hook.** Transient vs. poison vs. fatal is storage-specific knowledge. Inject an `ErrorClassifier`; the default only knows generic exceptions (and maps unknowns to poison — probe-protected bisect, never silent drops). A production-hardened SQLAlchemy classifier is in [`examples/sqlite_sink/`](examples/sqlite_sink/).
49
- - **Backpressure with hysteresis.** Ingest probes consumer health on a side channel and rejects based on backlog age with separate trip/recover thresholds. The core ships zero-I/O signals (`ManualBackpressureSignal`); the HTTP-probe reference is in [`examples/http_probe/`](examples/http_probe/).
55
+ - **A uniqueness contract without a mandated store.** The admission flow (existence check → atomic slot reservation → cold-entity backfill → consumer-side authoritative refresh) is framework machinery. Whether the existence store is Redis, an in-process dict, or something else is your policy: implement `AdmissionPolicy`, or use the built-in zero-I/O shortcuts (`"none"`, `"in-memory"`). A production-grade Redis implementation ships as [`streamgate.contrib.redis_admission`](#contrib-official-strategy-implementations).
56
+ - **One sink hook, any storage.** `ConsumeSpec(sink=...)` takes a `RecordWriter` — write to SQL, Elasticsearch, another service, a file; the framework handles batching, retries, error classification and DLQ bisection around it. SQL upsert implementations ship as [`streamgate.contrib.sql_sink` / `sqlite_sink` / `mssql_sink`](#contrib-official-strategy-implementations).
57
+ - **Error classification as a hook.** Transient vs. poison vs. fatal is storage-specific knowledge. Inject an `ErrorClassifier`; the default only knows generic exceptions (and maps unknowns to poison — probe-protected bisect, never silent drops). A production-hardened SQLAlchemy classifier ships as `streamgate.contrib.sql_sink.SQLAlchemyErrorClassifier`.
58
+ - **Backpressure with hysteresis.** Ingest probes consumer health on a side channel and rejects based on backlog age with separate trip/recover thresholds. The core ships zero-I/O signals (`ManualBackpressureSignal`); the HTTP-probe implementation ships as [`streamgate.contrib.http_probe`](#contrib-official-strategy-implementations).
50
59
  - **DLQ bisection instead of head-of-line blocking.** A poison message never wedges a partition: the failing batch is bisected with probe comparison, the bad record is quarantined to a dead-letter topic, the rest commits.
51
60
 
52
61
  ## Quick Start
@@ -130,26 +139,48 @@ your HTTP app (presentation is yours: auth/routing/OpenAPI)
130
139
  └─ health snapshot (expose it with your own web framework)
131
140
  ```
132
141
 
133
- The package contains **zero web-framework code** (no fastapi/uvicorn) and **zero DB/Redis code** — enforced by an import-linter forbidden contract in CI.
142
+ The package contains **zero web-framework code** (no fastapi/uvicorn), and the core layers never import `streamgate.contrib` — enforced by the import-linter layered contract in CI.
134
143
 
135
- ## Installation
144
+ ## Contrib: official strategy implementations
136
145
 
137
- ```bash
138
- pip install streamgate # aiokafka + loguru + pydantic. That's it.
146
+ `streamgate.contrib` is the official home for I/O strategy implementations on top of the core protocols. Everything below ships in the wheel; third-party dependencies are opt-in via extras, and importing without the extra raises an error that tells you which extra to install.
147
+
148
+ | Extra | Install | Subpackage(s) | Provides |
149
+ |-------|---------|---------------|----------|
150
+ | `[redis]` | `pip install "streamgate[redis]"` | `contrib.redis_admission` | `RedisExistenceAdmission` (`AdmissionPolicy` impl), `RedisExistenceCache`, `RedisConfig` |
151
+ | `[sql]` | `pip install "streamgate[sql]"` | `contrib.sql_sink` / `contrib.sqlite_sink` / `contrib.mssql_sink` | `Upsert`/`UpsertWriter` (`RecordWriter` impl, SQLite `ON CONFLICT` + MSSQL `MERGE`+HOLDLOCK), engine factory, `SqlBackfill` (`BackfillSource` impl), `SQLAlchemyErrorClassifier`, `DbConfig` |
152
+ | `[http]` | `pip install "streamgate[http]"` | `contrib.http_probe` | `HttpProbeSignal` / `HysteresisController` (`BackpressureSignal` impl with trip/recover hysteresis, fail-closed probing) |
153
+
154
+ ```python
155
+ from streamgate import IngestBinding
156
+ from streamgate.contrib.redis_admission import RedisConfig, RedisExistenceAdmission, RedisExistenceCache
157
+
158
+ binding = IngestBinding(
159
+ message_type="order",
160
+ entity_key=lambda r: r.order_id,
161
+ slot_key=lambda r: "order",
162
+ summary=lambda r: {"amount": r.amount},
163
+ admission=RedisExistenceAdmission(
164
+ cache=RedisExistenceCache(RedisConfig(url="redis://localhost:6379/0")),
165
+ entity_key=lambda r: r.order_id,
166
+ slot_key=lambda r: "order",
167
+ summary=lambda r: {"amount": r.amount},
168
+ ),
169
+ )
139
170
  ```
140
171
 
141
- No extras. For storage/backpressure carriers, copy from `examples/` and add the
142
- third-party library your implementation needs (e.g. `redis`, `sqlalchemy`,
143
- `httpx`) to *your* project:
172
+ **Stability:** contrib packages are provisional (beta-grade) — usable in production (these implementations ran in production before being merged), but signatures may still adjust within minor releases. The core API contract is unaffected: the core never imports contrib.
144
173
 
145
- | Example | Demonstrates | Extra deps for the example |
146
- |---------|--------------|----------------------------|
147
- | [`pure_pipeline/`](examples/pure_pipeline/) (main) | bare-install full round trip, `in-memory` admission, `on_record` stdlib SQLite sink | none |
148
- | [`sqlite_sink/`](examples/sqlite_sink/) | `RecordWriter` injection with the SQL upsert sugar + DB error classifier | `sqlalchemy`, `sqlmodel`, `aiosqlite` |
149
- | [`redis_admission/`](examples/redis_admission/) | `AdmissionPolicy` injection with Redis existence cache | `redis` |
150
- | [`http_probe/`](examples/http_probe/) | `BackpressureSignal` injection with hysteresis | `httpx` |
174
+ ## Installation
175
+
176
+ ```bash
177
+ pip install streamgate # aiokafka + loguru + pydantic. That's it.
178
+ pip install "streamgate[redis]" # + Redis admission
179
+ pip install "streamgate[sql]" # + SQL upsert sink (SQLite + MSSQL)
180
+ pip install "streamgate[http]" # + HTTP probe backpressure
181
+ ```
151
182
 
152
- Other databases (PostgreSQL, MySQL, ...): implement `RecordWriter` (Tier 1) — the same hook every built path uses.
183
+ For anything else (PostgreSQL, Elasticsearch, your own probe...), implement the protocol yourself (Tier 1) — the same hook every contrib package uses.
153
184
 
154
185
  ## Configuration
155
186
 
@@ -157,13 +188,13 @@ All components configure via typed objects that map 1:1 to environment variables
157
188
 
158
189
  ## Examples
159
190
 
160
- See [`examples/`](examples/) for four runnable pipelines plus a `docker-compose.yml` (Kafka + Redis). Start with [`examples/pure_pipeline/`](examples/pure_pipeline/) — it runs with nothing but `pip install streamgate`.
191
+ See [`examples/`](examples/) for five runnable pipelines plus a `docker-compose.yml` (Kafka + Redis). Start with [`examples/pure_pipeline/`](examples/pure_pipeline/) — it runs with nothing but `pip install streamgate`. The strategy demos (`redis_admission/`, `sqlite_sink/`, `http_probe/`) import from `streamgate.contrib`, and [`examples/prod_pipeline/`](examples/prod_pipeline/) wires the full production topology (Redis admission + HTTP-probe backpressure + MSSQL sink) in one demo.
161
192
 
162
193
  ## Roadmap
163
194
 
164
195
  - Test suite (first release ships without tests; APIs are exercised in production but the project considers this its top debt)
165
196
  - Documentation site
166
- - More admission/sink reference implementations in `examples/`
197
+ - More admission/sink implementations in `streamgate.contrib`
167
198
 
168
199
  ## License
169
200
 
@@ -9,17 +9,17 @@ English | [中文](README.zh-CN.md)
9
9
 
10
10
  **streamgate** is a pure-core Kafka data pipeline framework: conditional admission at the door, reliable delivery through Kafka, and a single typed hook — `RecordWriter` — wherever you want the data to land.
11
11
 
12
- The wheel installs exactly three dependencies (`aiokafka`, `loguru`, `pydantic`) and contains **zero database, Redis or HTTP-client code**. All I/O strategies ship as copy-paste examples in [`examples/`](examples/).
12
+ The wheel installs exactly three dependencies (`aiokafka`, `loguru`, `pydantic`) and the core contains **zero database, Redis or HTTP-client code**. Official I/O strategy implementations live in [`streamgate.contrib`](#contrib-official-strategy-implementations) — opt-in via extras — and runnable demos live in [`examples/`](examples/).
13
13
 
14
14
  - 中文提示:本仓库对外文档以英文为主;内部代码注释保留中文。
15
15
 
16
16
  ## Why streamgate
17
17
 
18
18
  - **Mechanisms in the framework, policies in your hands.** The framework owns admission *orchestration*, backpressure, health probing, reconnection, offset handling, batch buffering and graceful shutdown. You own entity/slot semantics, schemas, and — critically — **the storage carriers**: your database, your Redis, your HTTP probe are injected via typed protocols, never imported by the core.
19
- - **A uniqueness contract without a mandated store.** The admission flow (existence check → atomic slot reservation → cold-entity backfill → consumer-side authoritative refresh) is framework machinery. Whether the existence store is Redis, an in-process dict, or something else is your policy: implement `AdmissionPolicy`, or use the built-in zero-I/O shortcuts (`"none"`, `"in-memory"`). A production-grade Redis reference lives in [`examples/redis_admission/`](examples/redis_admission/).
20
- - **One sink hook, any storage.** `ConsumeSpec(sink=...)` takes a `RecordWriter` — write to SQL, Elasticsearch, another service, a file; the framework handles batching, retries, error classification and DLQ bisection around it. No database is built in, on purpose.
21
- - **Error classification as a hook.** Transient vs. poison vs. fatal is storage-specific knowledge. Inject an `ErrorClassifier`; the default only knows generic exceptions (and maps unknowns to poison — probe-protected bisect, never silent drops). A production-hardened SQLAlchemy classifier is in [`examples/sqlite_sink/`](examples/sqlite_sink/).
22
- - **Backpressure with hysteresis.** Ingest probes consumer health on a side channel and rejects based on backlog age with separate trip/recover thresholds. The core ships zero-I/O signals (`ManualBackpressureSignal`); the HTTP-probe reference is in [`examples/http_probe/`](examples/http_probe/).
19
+ - **A uniqueness contract without a mandated store.** The admission flow (existence check → atomic slot reservation → cold-entity backfill → consumer-side authoritative refresh) is framework machinery. Whether the existence store is Redis, an in-process dict, or something else is your policy: implement `AdmissionPolicy`, or use the built-in zero-I/O shortcuts (`"none"`, `"in-memory"`). A production-grade Redis implementation ships as [`streamgate.contrib.redis_admission`](#contrib-official-strategy-implementations).
20
+ - **One sink hook, any storage.** `ConsumeSpec(sink=...)` takes a `RecordWriter` — write to SQL, Elasticsearch, another service, a file; the framework handles batching, retries, error classification and DLQ bisection around it. SQL upsert implementations ship as [`streamgate.contrib.sql_sink` / `sqlite_sink` / `mssql_sink`](#contrib-official-strategy-implementations).
21
+ - **Error classification as a hook.** Transient vs. poison vs. fatal is storage-specific knowledge. Inject an `ErrorClassifier`; the default only knows generic exceptions (and maps unknowns to poison — probe-protected bisect, never silent drops). A production-hardened SQLAlchemy classifier ships as `streamgate.contrib.sql_sink.SQLAlchemyErrorClassifier`.
22
+ - **Backpressure with hysteresis.** Ingest probes consumer health on a side channel and rejects based on backlog age with separate trip/recover thresholds. The core ships zero-I/O signals (`ManualBackpressureSignal`); the HTTP-probe implementation ships as [`streamgate.contrib.http_probe`](#contrib-official-strategy-implementations).
23
23
  - **DLQ bisection instead of head-of-line blocking.** A poison message never wedges a partition: the failing batch is bisected with probe comparison, the bad record is quarantined to a dead-letter topic, the rest commits.
24
24
 
25
25
  ## Quick Start
@@ -103,26 +103,48 @@ your HTTP app (presentation is yours: auth/routing/OpenAPI)
103
103
  └─ health snapshot (expose it with your own web framework)
104
104
  ```
105
105
 
106
- The package contains **zero web-framework code** (no fastapi/uvicorn) and **zero DB/Redis code** — enforced by an import-linter forbidden contract in CI.
106
+ The package contains **zero web-framework code** (no fastapi/uvicorn), and the core layers never import `streamgate.contrib` — enforced by the import-linter layered contract in CI.
107
107
 
108
- ## Installation
108
+ ## Contrib: official strategy implementations
109
109
 
110
- ```bash
111
- pip install streamgate # aiokafka + loguru + pydantic. That's it.
110
+ `streamgate.contrib` is the official home for I/O strategy implementations on top of the core protocols. Everything below ships in the wheel; third-party dependencies are opt-in via extras, and importing without the extra raises an error that tells you which extra to install.
111
+
112
+ | Extra | Install | Subpackage(s) | Provides |
113
+ |-------|---------|---------------|----------|
114
+ | `[redis]` | `pip install "streamgate[redis]"` | `contrib.redis_admission` | `RedisExistenceAdmission` (`AdmissionPolicy` impl), `RedisExistenceCache`, `RedisConfig` |
115
+ | `[sql]` | `pip install "streamgate[sql]"` | `contrib.sql_sink` / `contrib.sqlite_sink` / `contrib.mssql_sink` | `Upsert`/`UpsertWriter` (`RecordWriter` impl, SQLite `ON CONFLICT` + MSSQL `MERGE`+HOLDLOCK), engine factory, `SqlBackfill` (`BackfillSource` impl), `SQLAlchemyErrorClassifier`, `DbConfig` |
116
+ | `[http]` | `pip install "streamgate[http]"` | `contrib.http_probe` | `HttpProbeSignal` / `HysteresisController` (`BackpressureSignal` impl with trip/recover hysteresis, fail-closed probing) |
117
+
118
+ ```python
119
+ from streamgate import IngestBinding
120
+ from streamgate.contrib.redis_admission import RedisConfig, RedisExistenceAdmission, RedisExistenceCache
121
+
122
+ binding = IngestBinding(
123
+ message_type="order",
124
+ entity_key=lambda r: r.order_id,
125
+ slot_key=lambda r: "order",
126
+ summary=lambda r: {"amount": r.amount},
127
+ admission=RedisExistenceAdmission(
128
+ cache=RedisExistenceCache(RedisConfig(url="redis://localhost:6379/0")),
129
+ entity_key=lambda r: r.order_id,
130
+ slot_key=lambda r: "order",
131
+ summary=lambda r: {"amount": r.amount},
132
+ ),
133
+ )
112
134
  ```
113
135
 
114
- No extras. For storage/backpressure carriers, copy from `examples/` and add the
115
- third-party library your implementation needs (e.g. `redis`, `sqlalchemy`,
116
- `httpx`) to *your* project:
136
+ **Stability:** contrib packages are provisional (beta-grade) — usable in production (these implementations ran in production before being merged), but signatures may still adjust within minor releases. The core API contract is unaffected: the core never imports contrib.
117
137
 
118
- | Example | Demonstrates | Extra deps for the example |
119
- |---------|--------------|----------------------------|
120
- | [`pure_pipeline/`](examples/pure_pipeline/) (main) | bare-install full round trip, `in-memory` admission, `on_record` stdlib SQLite sink | none |
121
- | [`sqlite_sink/`](examples/sqlite_sink/) | `RecordWriter` injection with the SQL upsert sugar + DB error classifier | `sqlalchemy`, `sqlmodel`, `aiosqlite` |
122
- | [`redis_admission/`](examples/redis_admission/) | `AdmissionPolicy` injection with Redis existence cache | `redis` |
123
- | [`http_probe/`](examples/http_probe/) | `BackpressureSignal` injection with hysteresis | `httpx` |
138
+ ## Installation
139
+
140
+ ```bash
141
+ pip install streamgate # aiokafka + loguru + pydantic. That's it.
142
+ pip install "streamgate[redis]" # + Redis admission
143
+ pip install "streamgate[sql]" # + SQL upsert sink (SQLite + MSSQL)
144
+ pip install "streamgate[http]" # + HTTP probe backpressure
145
+ ```
124
146
 
125
- Other databases (PostgreSQL, MySQL, ...): implement `RecordWriter` (Tier 1) — the same hook every built path uses.
147
+ For anything else (PostgreSQL, Elasticsearch, your own probe...), implement the protocol yourself (Tier 1) — the same hook every contrib package uses.
126
148
 
127
149
  ## Configuration
128
150
 
@@ -130,13 +152,13 @@ All components configure via typed objects that map 1:1 to environment variables
130
152
 
131
153
  ## Examples
132
154
 
133
- See [`examples/`](examples/) for four runnable pipelines plus a `docker-compose.yml` (Kafka + Redis). Start with [`examples/pure_pipeline/`](examples/pure_pipeline/) — it runs with nothing but `pip install streamgate`.
155
+ See [`examples/`](examples/) for five runnable pipelines plus a `docker-compose.yml` (Kafka + Redis). Start with [`examples/pure_pipeline/`](examples/pure_pipeline/) — it runs with nothing but `pip install streamgate`. The strategy demos (`redis_admission/`, `sqlite_sink/`, `http_probe/`) import from `streamgate.contrib`, and [`examples/prod_pipeline/`](examples/prod_pipeline/) wires the full production topology (Redis admission + HTTP-probe backpressure + MSSQL sink) in one demo.
134
156
 
135
157
  ## Roadmap
136
158
 
137
159
  - Test suite (first release ships without tests; APIs are exercised in production but the project considers this its top debt)
138
160
  - Documentation site
139
- - More admission/sink reference implementations in `examples/`
161
+ - More admission/sink implementations in `streamgate.contrib`
140
162
 
141
163
  ## License
142
164
 
@@ -14,14 +14,14 @@
14
14
  查重、限流、背压 批量缓冲、重试、坏数据隔离
15
15
  ```
16
16
 
17
- 框架只装 3 个依赖(`aiokafka` / `loguru` / `pydantic`),**包里没有一行数据库、Redis、HTTP 客户端代码**——所有"数据落到哪、用什么存"的实现都以可复制的示例放在 [`examples/`](examples/)。
17
+ 框架核心只装 3 个依赖(`aiokafka` / `loguru` / `pydantic`),核心层**没有一行数据库、Redis、HTTP 客户端代码**。官方 I/O 策略实现收录在 [`streamgate.contrib`](#不想自己写用-contrib)——按 extras 按需安装、装完即可 import;可运行的完整演示在 [`examples/`](examples/)。
18
18
 
19
19
  ---
20
20
 
21
21
  ## 为什么选它
22
22
 
23
23
  - **机制归框架,策略归你。** 查重流程、重试、限流、断线重连、优雅停机这些"脏活累活"框架全包;你只管业务语义(主键是什么、摘要放什么)和存储选型。
24
- - **查重不绑定存储。** 框架提供查重的"流程编排"(存在性判定 → 原子占位 → 冷回源 → 消费后刷新),至于用 Redis、进程内字典还是别的什么来存,由你注入。内置零依赖实现开箱即用,生产级 Redis 参考实现在 examples 里抄。
24
+ - **查重不绑定存储。** 框架提供查重的"流程编排"(存在性判定 → 原子占位 → 冷回源 → 消费后刷新),至于用 Redis、进程内字典还是别的什么来存,由你注入。内置零依赖实现开箱即用;生产级 Redis 实现已收录为 `streamgate.contrib.redis_admission`(装 `[redis]` extra 即用)。
25
25
  - **一个写入钩子,接任何存储。** MySQL、ES、另一个服务、一个文件……实现 `RecordWriter` 接口注入即可。批量缓冲、失败重试、坏数据二分隔离这些框架替你兜底。
26
26
  - **错误分类也是钩子。** "连接超时该重试"还是"数据脏了该隔离",只有懂你存储的人知道。注入一个分类器即可;不注入也有安全的默认行为(不会丢数据)。
27
27
  - **背压带磁滞防抖。** 消费端积压超标自动拒绝接收,回落后自动放行,来回抖动有磁滞带压着。
@@ -201,7 +201,7 @@ admission = MyAdmission(backfill=MyBackfill()) # 回源传给你的策略,不
201
201
  | 配了,权威库说"确实没有" | 放行(确认是新数据) |
202
202
  | 配了,但权威库也挂了 | 仍拒绝——核实不了绝不猜 |
203
203
 
204
- 两个内置准入策略都用不上回源:`NoAdmission` 压根不查重;`InMemoryAdmission` 的进程内字典本身就是权威(查不到 = 真的没有),代价是仅单进程有效、重启即清空——一旦多副本部署,就需要 Redis 查重 + 回源的两级结构(组合参考见 [`examples/redis_admission/`](examples/redis_admission/) + [`examples/sqlite_sink/backfill.py`](examples/sqlite_sink/backfill.py))。
204
+ 两个内置准入策略都用不上回源:`NoAdmission` 压根不查重;`InMemoryAdmission` 的进程内字典本身就是权威(查不到 = 真的没有),代价是仅单进程有效、重启即清空——一旦多副本部署,就需要 Redis 查重 + 回源的两级结构(`streamgate.contrib.redis_admission` + `streamgate.contrib.sql_sink.SqlBackfill` 已内置这套组合)。
205
205
 
206
206
  ### 例 4:自定义背压信号(跟着消费端积压走)
207
207
 
@@ -224,30 +224,56 @@ class MySignal: # 不用继承,duck typing
224
224
  gateway = IngestGateway(..., signal=MySignal()) # 注入即可
225
225
  ```
226
226
 
227
- 不注入会怎样?默认 `ManualBackpressureSignal`——纯手动开关,永远放行;维护窗口可把 `signal.rejecting` 翻成 `True` 手动挡闸。接收端与消费端不在同一进程时,框架"看不见"积压,需要你自己写探针:参考 [`examples/http_probe/`](examples/http_probe/)(积压探针 + 磁滞限流状态机)。
227
+ 不注入会怎样?默认 `ManualBackpressureSignal`——纯手动开关,永远放行;维护窗口可把 `signal.rejecting` 翻成 `True` 手动挡闸。接收端与消费端不在同一进程时,框架"看不见"积压,用 `streamgate.contrib.http_probe`(积压探针 + 磁滞限流状态机,装 `[http]` extra)。
228
228
 
229
- ### 不想自己写?去 examples 抄
229
+ ### 不想自己写?用 contrib
230
230
 
231
- 每个例子都是**生产级质量、复制即用**(纳入 CI lint/类型检查,腐化会被拦下):
231
+ 官方策略实现随 wheel 发布,按 extras 装依赖,**import 即用**(与核心同门禁:CI lint/类型检查全覆盖):
232
232
 
233
- | 目录 | 给你什么 | 示例自身的额外依赖 |
234
- |---|---|---|
235
- | [`pure_pipeline/`](examples/pure_pipeline/) | 零依赖全链路(**主示例**,裸装跑通就是纯度承诺的活体证明) | 无 |
236
- | [`sqlite_sink/`](examples/sqlite_sink/) | SQL 幂等落库全套(引擎/方言/回源)+ 数据库异常分类器 | sqlalchemy、sqlmodel、aiosqlite |
237
- | [`redis_admission/`](examples/redis_admission/) | Redis 分布式查重(多实例安全,含 Lua 原子占位) | redis |
238
- | [`http_probe/`](examples/http_probe/) | 积压探针 + 磁滞限流状态机 | httpx |
233
+ | Extra | 安装 | 子包 | 给你什么 |
234
+ |---|---|---|---|
235
+ | `[redis]` | `pip install "streamgate[redis]"` | `streamgate.contrib.redis_admission` | Redis 分布式查重(多实例安全,Lua 原子占位/冷回源/空实体哨兵/fail-closed) |
236
+ | `[sql]` | `pip install "streamgate[sql]"` | `streamgate.contrib.sqlite_sink` / `.mssql_sink` / `.sql_sink` | SQL 幂等落库全套(SQLite ON CONFLICT / MSSQL MERGE+HOLDLOCK、引擎工厂、`SqlBackfill` 回源)+ 数据库异常分类器 |
237
+ | `[http]` | `pip install "streamgate[http]"` | `streamgate.contrib.http_probe` | 积压探针 + 磁滞限流状态机(trip/recover 防抖、fail-closed) |
238
+
239
+ ```python
240
+ from streamgate import IngestBinding
241
+ from streamgate.contrib.redis_admission import (
242
+ RedisConfig, RedisExistenceAdmission, RedisExistenceCache,
243
+ )
244
+
245
+ binding = IngestBinding(
246
+ message_type="order",
247
+ entity_key=lambda r: r.order_id,
248
+ slot_key=lambda r: "order",
249
+ summary=lambda r: {"amount": r.amount},
250
+ admission=RedisExistenceAdmission(
251
+ cache=RedisExistenceCache(RedisConfig(url="redis://localhost:6379/0")),
252
+ entity_key=lambda r: r.order_id,
253
+ slot_key=lambda r: "order",
254
+ summary=lambda r: {"amount": r.amount},
255
+ ),
256
+ )
257
+ ```
258
+
259
+ 缺对应 extra 时 import 会报错并提示该装哪个 extras,不会出现裸的 `ModuleNotFoundError`。
260
+
261
+ **稳定性**:contrib 为 provisional(Beta 级)——可直接上生产(这些实现并入前已生产验证),但小版本内允许调整签名;核心 API 契约不受影响(核心层永不 import contrib)。
239
262
 
240
- 仓库根的 [`examples/docker-compose.yml`](examples/docker-compose.yml) 提供 Kafka + Redis 双服务。
263
+ 仓库根的 [`examples/docker-compose.yml`](examples/docker-compose.yml) 提供 Kafka + Redis 双服务;五个可运行演示见 [`examples/`](examples/)(策略演示直接 import contrib;[`examples/prod_pipeline/`](examples/prod_pipeline/) 把 Redis 准入 + HTTP 探活 + MSSQL 落库的生产拓扑一次接全)。
241
264
 
242
265
  ---
243
266
 
244
267
  ## 安装
245
268
 
246
269
  ```bash
247
- pip install streamgate # 只装 aiokafka + loguru + pydantic,没有 extras
270
+ pip install streamgate # 只装 aiokafka + loguru + pydantic
271
+ pip install "streamgate[redis]" # + Redis 分布式查重
272
+ pip install "streamgate[sql]" # + SQL 幂等落库(SQLite + MSSQL)
273
+ pip install "streamgate[http]" # + HTTP 探活背压
248
274
  ```
249
275
 
250
- 存储/探针载体:从 `examples/` 复制对应模块,第三方库(redis、sqlalchemy、httpx 等)装进**你自己的项目**。其他数据库(PostgreSQL、MySQL……):自己实现 `RecordWriter`——所有内置路径用的也是同一个钩子。
276
+ 其他数据库(PostgreSQL、MySQL……):自己实现 `RecordWriter`——contrib 内置路径用的也是同一个钩子。
251
277
 
252
278
  ## 配置
253
279
 
@@ -271,13 +297,13 @@ pip install streamgate # 只装 aiokafka + loguru + pydantic,没有 extras
271
297
  └─ 健康快照(用你自己的 Web 框架暴露)
272
298
  ```
273
299
 
274
- 包内禁止任何 sqlalchemy / sqlmodel / redis / aiosqlite / aioodbc / httpx 导入——由 import-linter forbidden 契约在 CI 强制拦截。
300
+ 核心层禁止 import `streamgate.contrib`(contrib 反向依赖核心是合法的)——由 import-linter 分层契约在 CI 强制拦截;contrib 的第三方依赖全部走 extras 可选声明,裸装 `pip install streamgate` 不携带。
275
301
 
276
302
  ## 路线图
277
303
 
278
304
  - 测试套件(首发暂无测试;API 已在生产验证,项目视其为首要技术债)
279
305
  - 文档站
280
- - 更多 examples 参考实现
306
+ - 更多 `streamgate.contrib` 策略实现
281
307
 
282
308
  ## 许可证
283
309
 
@@ -3,13 +3,13 @@ services:
3
3
  image: apache/kafka:3.9.0
4
4
  container_name: streamgate-kafka
5
5
  ports:
6
- - "29092:29092"
6
+ - "9092:9092"
7
7
  environment:
8
8
  KAFKA_NODE_ID: 1
9
9
  KAFKA_PROCESS_ROLES: broker,controller
10
10
  KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
11
- KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_HOST://:29092
12
- KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
11
+ KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
12
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
13
13
  KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
14
14
  KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
15
15
  KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT