bazis-async-background 2.2.2__tar.gz → 2.2.3__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.
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/PKG-INFO +1 -1
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/_version.py +3 -3
- bazis_async_background-2.2.3/bazis/contrib/async_background/broker.py +159 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/conf.py +25 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/PKG-INFO +1 -1
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/SOURCES.txt +1 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/Dockerfile +9 -8
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/demo/tasks.py +6 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/docker-compose.test.yml +0 -38
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/main.py +0 -3
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/test.env +2 -0
- bazis_async_background-2.2.3/sample/uv.lock +1690 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/tests/test_demo.py +7 -0
- bazis_async_background-2.2.2/bazis/contrib/async_background/broker.py +0 -62
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/.dockerignore +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/.github/workflows/build-publish.yml +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/.github/workflows/update-licenses.yml +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/.gitignore +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/README.md +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/__init__.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/apps.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/management/__init__.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/management/commands/__init__.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/management/commands/kafka_consumer_multiple.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/management/commands/kafka_consumer_single.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/producer.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/router.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/routes.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/schemas.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/utils.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/dependency_links.txt +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/requires.txt +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/top_level.txt +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/pyproject.toml +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/pytest.ini +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/demo/__init__.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/demo/router.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/demo/schemas.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/manage.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/project.env +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/pyproject.toml +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/__init__.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/router.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/settings.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/urls.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/wsgi.py +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/setup.cfg +0 -0
- {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/tests/conftest.py +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '2.2.
|
|
32
|
-
__version_tuple__ = version_tuple = (2, 2,
|
|
31
|
+
__version__ = version = '2.2.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 2, 3)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g188af1a56'
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Copyright 2026 EcoFuture Technology Services LLC and contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import random
|
|
18
|
+
from contextlib import asynccontextmanager
|
|
19
|
+
|
|
20
|
+
from aiokafka.admin import AIOKafkaAdminClient, NewTopic
|
|
21
|
+
from aiokafka.errors import TopicAlreadyExistsError, NoError
|
|
22
|
+
from django.conf import settings
|
|
23
|
+
from faststream import FastStream
|
|
24
|
+
from faststream.kafka import KafkaBroker
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_brokers_by_loop_id: dict[int, KafkaBroker] = {}
|
|
30
|
+
_consumer_broker: KafkaBroker | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _new_broker() -> KafkaBroker:
|
|
34
|
+
return KafkaBroker(settings.KAFKA_BOOTSTRAP_SERVERS)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_broker_for_async() -> KafkaBroker:
|
|
38
|
+
loop_id = id(asyncio.get_running_loop())
|
|
39
|
+
broker = _brokers_by_loop_id.get(loop_id)
|
|
40
|
+
if broker is None:
|
|
41
|
+
broker = _new_broker()
|
|
42
|
+
_brokers_by_loop_id[loop_id] = broker
|
|
43
|
+
return broker
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_broker_for_consumer() -> KafkaBroker:
|
|
47
|
+
global _consumer_broker
|
|
48
|
+
if _consumer_broker is None:
|
|
49
|
+
_consumer_broker = _new_broker()
|
|
50
|
+
return _consumer_broker
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@asynccontextmanager
|
|
54
|
+
async def lifespan_handler(app: FastStream | None = None):
|
|
55
|
+
stop_task = asyncio.create_task(
|
|
56
|
+
asyncio.sleep(
|
|
57
|
+
settings.KAFKA_CONSUMER_LIFETIME_SEC +
|
|
58
|
+
random.randint(0, settings.KAFKA_CONSUMER_LIFETIME_JITTER_SEC)
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
yield
|
|
62
|
+
stop_task.cancel()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_app() -> FastStream:
|
|
66
|
+
return FastStream(get_broker_for_consumer(), lifespan=lifespan_handler)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@asynccontextmanager
|
|
70
|
+
async def _get_admin_client():
|
|
71
|
+
admin_client = AIOKafkaAdminClient(
|
|
72
|
+
bootstrap_servers=settings.KAFKA_BOOTSTRAP_SERVERS,
|
|
73
|
+
request_timeout_ms=settings.KAFKA_ADMIN_TIMEOUT_MS,
|
|
74
|
+
)
|
|
75
|
+
started = False
|
|
76
|
+
try:
|
|
77
|
+
await admin_client.start()
|
|
78
|
+
started = True
|
|
79
|
+
yield admin_client
|
|
80
|
+
finally:
|
|
81
|
+
if started:
|
|
82
|
+
await admin_client.close()
|
|
83
|
+
else:
|
|
84
|
+
try:
|
|
85
|
+
await admin_client.close()
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.debug(
|
|
88
|
+
"Kafka admin client close failed after failed start.",
|
|
89
|
+
exc_info=True,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def ensure_topic_exists(
|
|
94
|
+
topic_name: str,
|
|
95
|
+
num_partitions: int | None = None,
|
|
96
|
+
replication_factor: int | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
logger.info("Ensuring Kafka topic '%s' exists...", topic_name)
|
|
99
|
+
|
|
100
|
+
resolved_num_partitions = num_partitions or settings.KAFKA_AUTO_TOPIC_NUM_PARTITIONS
|
|
101
|
+
resolved_replication_factor = replication_factor or settings.KAFKA_AUTO_TOPIC_REPLICATION_FACTOR
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
async with _get_admin_client() as admin_client:
|
|
105
|
+
topics = await admin_client.list_topics()
|
|
106
|
+
if topic_name in topics:
|
|
107
|
+
logger.info("Kafka topic '%s' already exists.", topic_name)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
new_topic = NewTopic(
|
|
111
|
+
name=topic_name,
|
|
112
|
+
num_partitions=resolved_num_partitions,
|
|
113
|
+
replication_factor=resolved_replication_factor,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
resp = await admin_client.create_topics([new_topic])
|
|
117
|
+
|
|
118
|
+
for err in getattr(resp, "topic_errors", []):
|
|
119
|
+
# err[0]/err[1]/err[2] topic/error_code/message
|
|
120
|
+
error_code = err[1]
|
|
121
|
+
if error_code == NoError.errno:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
if error_code == TopicAlreadyExistsError.errno:
|
|
125
|
+
logger.info("Kafka topic '%s' already exists.", topic_name)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
error_message = err[2] if len(err) > 2 else None
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
"Error creating Kafka topic '%s': code=%s message=%s"
|
|
131
|
+
% (topic_name, error_code, error_message)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
logger.info("Kafka topic '%s' created successfully.", topic_name)
|
|
135
|
+
except TopicAlreadyExistsError:
|
|
136
|
+
logger.info("Kafka topic '%s' already exists.", topic_name)
|
|
137
|
+
except Exception:
|
|
138
|
+
logger.exception("Error creating Kafka topic '%s'.", topic_name)
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def get_topics_by_prefix(
|
|
143
|
+
prefix: str,
|
|
144
|
+
) -> list[str]:
|
|
145
|
+
try:
|
|
146
|
+
async with _get_admin_client() as admin_client:
|
|
147
|
+
topics = await admin_client.list_topics()
|
|
148
|
+
|
|
149
|
+
topics = [t for t in topics if t.startswith(prefix)]
|
|
150
|
+
logger.info(
|
|
151
|
+
"Found %d topics with prefix '%s': %s",
|
|
152
|
+
len(topics),
|
|
153
|
+
prefix,
|
|
154
|
+
topics if topics else "none",
|
|
155
|
+
)
|
|
156
|
+
return topics
|
|
157
|
+
except Exception:
|
|
158
|
+
logger.exception("Kafka error while listing topics with prefix '%s'.", prefix)
|
|
159
|
+
raise
|
{bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/conf.py
RENAMED
|
@@ -74,6 +74,31 @@ class Settings(BazisSettings):
|
|
|
74
74
|
default=10, description="Timeout in seconds for producing a message to Kafka."
|
|
75
75
|
)
|
|
76
76
|
|
|
77
|
+
KAFKA_ADMIN_TIMEOUT_MS: int = Field(
|
|
78
|
+
default=2000,
|
|
79
|
+
description="Timeout for Kafka admin operations (in milliseconds).",
|
|
80
|
+
)
|
|
81
|
+
KAFKA_AUTO_TOPIC_NUM_PARTITIONS: int = Field(
|
|
82
|
+
default=15,
|
|
83
|
+
description="Default number of partitions for automatically created topics.",
|
|
84
|
+
)
|
|
85
|
+
KAFKA_AUTO_TOPIC_REPLICATION_FACTOR: int = Field(
|
|
86
|
+
default=1,
|
|
87
|
+
description="Default replication factor for automatically created topics.",
|
|
88
|
+
)
|
|
89
|
+
KAFKA_CONSUMER_TIMEOUT_MS: int = Field(
|
|
90
|
+
default=2000,
|
|
91
|
+
description="Timeout for Kafka consumer polling (in milliseconds).",
|
|
92
|
+
)
|
|
93
|
+
KAFKA_FETCH_MIN_BYTES: int | None = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description="Minimum bytes the broker should accumulate before replying to fetch requests.",
|
|
96
|
+
)
|
|
97
|
+
KAFKA_FETCH_MAX_WAIT_MS: int | None = Field(
|
|
98
|
+
default=None,
|
|
99
|
+
description="Maximum wait time in milliseconds for fetch requests when the minimum bytes are not reached.",
|
|
100
|
+
)
|
|
101
|
+
|
|
77
102
|
@computed_field
|
|
78
103
|
@property
|
|
79
104
|
def KAFKA_ENABLED(self) -> bool: # noqa: N802
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
FROM python:3.13-slim-bookworm AS base
|
|
3
3
|
|
|
4
4
|
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_BAZIS_ASYNC_BACKGROUND=0.0.0 \
|
|
5
|
-
|
|
5
|
+
UV_PROJECT_ENVIRONMENT=/opt/venv \
|
|
6
6
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
7
7
|
PYTHONUNBUFFERED=1 \
|
|
8
8
|
PYTHONIOENCODING=utf-8
|
|
@@ -32,14 +32,15 @@ WORKDIR $WORKDIR
|
|
|
32
32
|
# 2. Runtime stage
|
|
33
33
|
FROM base AS runtime
|
|
34
34
|
|
|
35
|
-
WORKDIR ${WORKDIR}/sample
|
|
36
|
-
COPY pyproject.toml ${WORKDIR}/
|
|
37
35
|
COPY bazis/ ${WORKDIR}/bazis/
|
|
38
|
-
COPY
|
|
39
|
-
|
|
36
|
+
COPY pyproject.toml ${WORKDIR}/
|
|
37
|
+
COPY sample/pyproject.toml sample/uv.lock ${WORKDIR}/sample/
|
|
38
|
+
|
|
39
|
+
WORKDIR ${WORKDIR}/sample
|
|
40
|
+
RUN uv sync --frozen
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
COPY
|
|
42
|
+
COPY sample/ ${WORKDIR}/sample/
|
|
43
|
+
COPY tests/ ${WORKDIR}/tests/
|
|
43
44
|
|
|
44
45
|
# 3. Test stage
|
|
45
|
-
FROM runtime AS test
|
|
46
|
+
FROM runtime AS test
|
|
@@ -30,7 +30,13 @@ _subscriber_kwargs: dict[str, object] = {
|
|
|
30
30
|
"auto_offset_reset": settings.KAFKA_AUTO_OFFSET_RESET,
|
|
31
31
|
"auto_commit": settings.KAFKA_ENABLE_AUTO_COMMIT,
|
|
32
32
|
"auto_commit_interval_ms": settings.KAFKA_AUTO_COMMIT_INTERVAL_MS,
|
|
33
|
+
"consumer_timeout_ms": settings.KAFKA_CONSUMER_TIMEOUT_MS,
|
|
33
34
|
}
|
|
35
|
+
if settings.KAFKA_FETCH_MIN_BYTES is not None:
|
|
36
|
+
_subscriber_kwargs["fetch_min_bytes"] = settings.KAFKA_FETCH_MIN_BYTES
|
|
37
|
+
if settings.KAFKA_FETCH_MAX_WAIT_MS is not None:
|
|
38
|
+
_subscriber_kwargs["fetch_max_wait_ms"] = settings.KAFKA_FETCH_MAX_WAIT_MS
|
|
39
|
+
|
|
34
40
|
if settings.KAFKA_GROUP_ID:
|
|
35
41
|
_subscriber_kwargs["group_id"] = settings.KAFKA_GROUP_ID
|
|
36
42
|
|
{bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/docker-compose.test.yml
RENAMED
|
@@ -62,19 +62,9 @@ services:
|
|
|
62
62
|
bazis-async-background-consumer-test:
|
|
63
63
|
working_dir: /app/sample
|
|
64
64
|
depends_on:
|
|
65
|
-
postgresql-test:
|
|
66
|
-
condition: service_healthy
|
|
67
|
-
redis-test:
|
|
68
|
-
condition: service_healthy
|
|
69
65
|
kafka-test:
|
|
70
66
|
condition: service_healthy
|
|
71
|
-
bazis-async-background-migrate:
|
|
72
|
-
condition: service_completed_successfully
|
|
73
67
|
image: bazis-async-background-sample:local
|
|
74
|
-
build:
|
|
75
|
-
context: ..
|
|
76
|
-
dockerfile: sample/Dockerfile
|
|
77
|
-
target: runtime
|
|
78
68
|
env_file:
|
|
79
69
|
- test.env
|
|
80
70
|
environment:
|
|
@@ -85,30 +75,6 @@ services:
|
|
|
85
75
|
uv run python manage.py kafka_consumer_multiple --consumers-count=2
|
|
86
76
|
"
|
|
87
77
|
|
|
88
|
-
bazis-async-background-migrate:
|
|
89
|
-
working_dir: /app/sample
|
|
90
|
-
depends_on:
|
|
91
|
-
postgresql-test:
|
|
92
|
-
condition: service_healthy
|
|
93
|
-
redis-test:
|
|
94
|
-
condition: service_healthy
|
|
95
|
-
kafka-test:
|
|
96
|
-
condition: service_healthy
|
|
97
|
-
image: bazis-async-background-sample:local
|
|
98
|
-
build:
|
|
99
|
-
context: ..
|
|
100
|
-
dockerfile: sample/Dockerfile
|
|
101
|
-
target: runtime
|
|
102
|
-
env_file:
|
|
103
|
-
- test.env
|
|
104
|
-
environment:
|
|
105
|
-
PYTHONPATH: "/app"
|
|
106
|
-
DJANGO_SETTINGS_MODULE: "sample.settings"
|
|
107
|
-
command: >
|
|
108
|
-
sh -c "
|
|
109
|
-
uv run python manage.py migrate
|
|
110
|
-
"
|
|
111
|
-
|
|
112
78
|
bazis-async-background-pytest:
|
|
113
79
|
working_dir: /app/sample
|
|
114
80
|
depends_on:
|
|
@@ -116,10 +82,6 @@ services:
|
|
|
116
82
|
condition: service_healthy
|
|
117
83
|
redis-test:
|
|
118
84
|
condition: service_healthy
|
|
119
|
-
kafka-test:
|
|
120
|
-
condition: service_healthy
|
|
121
|
-
bazis-async-background-migrate:
|
|
122
|
-
condition: service_completed_successfully
|
|
123
85
|
bazis-async-background-consumer-test:
|
|
124
86
|
condition: service_started
|
|
125
87
|
image: bazis-async-background-sample:test
|
|
@@ -9,6 +9,8 @@ BS_CACHES__DEFAULT__LOCATION=redis://redis-test:6379/0
|
|
|
9
9
|
BS_KAFKA_BOOTSTRAP_SERVERS='kafka-test:9092'
|
|
10
10
|
BS_KAFKA_TOPIC_ASYNC_BG=sample_local_async_background
|
|
11
11
|
BS_KAFKA_GROUP_ID=sample_local
|
|
12
|
+
BS_KAFKA_TASKS='["demo.tasks"]'
|
|
13
|
+
BS_KAFKA_AUTO_OFFSET_RESET=earliest
|
|
12
14
|
BS_KAFKA_CONSUMER_LIFETIME_SEC=3600
|
|
13
15
|
BS_KAFKA_CONSUMER_LIFETIME_JITTER_SEC=300
|
|
14
16
|
|