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.
Files changed (48) hide show
  1. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/PKG-INFO +1 -1
  2. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/_version.py +3 -3
  3. bazis_async_background-2.2.3/bazis/contrib/async_background/broker.py +159 -0
  4. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/conf.py +25 -0
  5. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/PKG-INFO +1 -1
  6. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/SOURCES.txt +1 -0
  7. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/Dockerfile +9 -8
  8. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/demo/tasks.py +6 -0
  9. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/docker-compose.test.yml +0 -38
  10. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/main.py +0 -3
  11. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/test.env +2 -0
  12. bazis_async_background-2.2.3/sample/uv.lock +1690 -0
  13. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/tests/test_demo.py +7 -0
  14. bazis_async_background-2.2.2/bazis/contrib/async_background/broker.py +0 -62
  15. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/.dockerignore +0 -0
  16. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/.github/workflows/build-publish.yml +0 -0
  17. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/.github/workflows/update-licenses.yml +0 -0
  18. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/.gitignore +0 -0
  19. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/README.md +0 -0
  20. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/__init__.py +0 -0
  21. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/apps.py +0 -0
  22. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/management/__init__.py +0 -0
  23. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/management/commands/__init__.py +0 -0
  24. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/management/commands/kafka_consumer_multiple.py +0 -0
  25. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/management/commands/kafka_consumer_single.py +0 -0
  26. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/producer.py +0 -0
  27. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/router.py +0 -0
  28. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/routes.py +0 -0
  29. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/schemas.py +0 -0
  30. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis/contrib/async_background/utils.py +0 -0
  31. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/dependency_links.txt +0 -0
  32. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/requires.txt +0 -0
  33. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/bazis_async_background.egg-info/top_level.txt +0 -0
  34. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/pyproject.toml +0 -0
  35. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/pytest.ini +0 -0
  36. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/demo/__init__.py +0 -0
  37. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/demo/router.py +0 -0
  38. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/demo/schemas.py +0 -0
  39. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/manage.py +0 -0
  40. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/project.env +0 -0
  41. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/pyproject.toml +0 -0
  42. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/__init__.py +0 -0
  43. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/router.py +0 -0
  44. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/settings.py +0 -0
  45. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/urls.py +0 -0
  46. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/sample/sample/wsgi.py +0 -0
  47. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/setup.cfg +0 -0
  48. {bazis_async_background-2.2.2 → bazis_async_background-2.2.3}/tests/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazis-async-background
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: Async Background core framework for Bazis.
5
5
  Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
6
6
  Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
@@ -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.2'
32
- __version_tuple__ = version_tuple = (2, 2, 2)
31
+ __version__ = version = '2.2.3'
32
+ __version_tuple__ = version_tuple = (2, 2, 3)
33
33
 
34
- __commit_id__ = commit_id = 'g4dc36df30'
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazis-async-background
3
- Version: 2.2.2
3
+ Version: 2.2.3
4
4
  Summary: Async Background core framework for Bazis.
5
5
  Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
6
6
  Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
@@ -30,6 +30,7 @@ sample/manage.py
30
30
  sample/project.env
31
31
  sample/pyproject.toml
32
32
  sample/test.env
33
+ sample/uv.lock
33
34
  sample/demo/__init__.py
34
35
  sample/demo/router.py
35
36
  sample/demo/schemas.py
@@ -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
- UV_SYSTEM_PYTHON=1 \
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 sample/pyproject.toml ${WORKDIR}/sample/
39
- RUN uv sync
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
- WORKDIR ${WORKDIR}
42
- COPY ./ ${WORKDIR}
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
 
@@ -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
@@ -18,6 +18,3 @@ import os
18
18
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings")
19
19
 
20
20
  from bazis.core.app import app
21
-
22
-
23
-
@@ -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