bazis-async-request 2.3.0__tar.gz → 2.4.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.
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/.github/workflows/release.yml +5 -2
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/.github/workflows/tests.yml +1 -1
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/CLAUDE.md +1 -1
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/PKG-INFO +14 -4
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/README.md +12 -2
- bazis_async_request-2.4.0/bazis/contrib/async_request/AGENTS.md +75 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/_version.py +3 -3
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/apps.py +5 -0
- bazis_async_request-2.4.0/bazis/contrib/async_request/bazis_manifest.toml +52 -0
- bazis_async_request-2.4.0/bazis/contrib/async_request/checks.py +70 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/middleware.py +11 -3
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/utils.py +7 -2
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/PKG-INFO +14 -4
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/SOURCES.txt +5 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/requires.txt +1 -1
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/scm_file_list.json +5 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/scm_version.json +2 -2
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/docs/releases/2.3.0.md +1 -1
- bazis_async_request-2.4.0/docs/releases/2.4.0.md +14 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/pyproject.toml +1 -1
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/test.env +1 -1
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_internal.py +47 -0
- bazis_async_request-2.4.0/tests/test_manifest.py +36 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/.dockerignore +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/.github/workflows/build-publish.yml +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/.github/workflows/update-licenses.yml +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/.gitignore +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/__init__.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/schemas.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/tasks.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/dependency_links.txt +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/top_level.txt +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/pytest.ini +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/Dockerfile +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/docker-compose.kafka.yml +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/docker-compose.test.yml +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/__init__.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/admin.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/migrations/0001_initial.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/migrations/0002_remove_order_fast_start__autogen_8f29b0_gin_and_more.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/migrations/__init__.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/models.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/router.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/routes.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/schemas.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/manage.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/project.env +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/pyproject.toml +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/sample/__init__.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/sample/main.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/sample/router.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/sample/settings.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/sample/urls.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/sample/wsgi.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/users/__init__.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/users/admin.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/users/migrations/0001_initial.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/users/migrations/__init__.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/users/models.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/uv.lock +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/setup.cfg +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/__init__.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/conftest.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_decorator.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_multiple_order_requests.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_order_access.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_order_patch.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_order_view.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_shop_calc_and_params.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_shop_force_background_methods.py +0 -0
- {bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/utils.py +0 -0
|
@@ -42,9 +42,12 @@ jobs:
|
|
|
42
42
|
echo "::error::Tag v$VERSION already exists"
|
|
43
43
|
exit 1
|
|
44
44
|
fi
|
|
45
|
-
|
|
45
|
+
# versionsort.suffix: a pre-release (v2.4.0-rc1) sorts before its release (v2.4.0)
|
|
46
|
+
LAST_TAG=$(git -c versionsort.suffix=- tag --list 'v*' --sort=-v:refname | head -n 1)
|
|
46
47
|
echo "Last release: ${LAST_TAG:-none}, new release: v$VERSION"
|
|
47
|
-
|
|
48
|
+
# sort -V orders "~" before anything, as versionsort.suffix does with "-"
|
|
49
|
+
NEWEST=$(printf '%s\n%s\n' "${LAST_TAG#v}" "$VERSION" | sed 's/-/~/' | sort -V | tail -n 1)
|
|
50
|
+
if [ -n "$LAST_TAG" ] && [ "$NEWEST" != "${VERSION/-/\~}" ]; then
|
|
48
51
|
echo "::error::Version $VERSION is not newer than $LAST_TAG"
|
|
49
52
|
exit 1
|
|
50
53
|
fi
|
|
@@ -38,7 +38,7 @@ to the database name): migrate it first (`python manage.py migrate` and
|
|
|
38
38
|
The tests marked `run_with_consumer` are skipped without Kafka settings. A local Kafka
|
|
39
39
|
without Docker: download the Kafka binaries and start a single KRaft node.
|
|
40
40
|
|
|
41
|
-
Lint: `ruff check bazis tests`. CI also runs `python manage.py makemigrations --check
|
|
41
|
+
Lint: `ruff check bazis tests sample`. CI also runs `python manage.py makemigrations --check
|
|
42
42
|
--dry-run` in `sample`: commit the migrations of model changes, including the sample apps.
|
|
43
43
|
|
|
44
44
|
## Releasing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bazis-async-request
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Async Background Requests module for Bazis framework.
|
|
5
5
|
Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
6
6
|
Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Framework :: Django
|
|
|
17
17
|
Classifier: Framework :: FastAPI
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
|
-
Requires-Dist: bazis>=2.
|
|
20
|
+
Requires-Dist: bazis>=2.4.0
|
|
21
21
|
Requires-Dist: bazis-async-background>=2.3.0
|
|
22
22
|
Provides-Extra: test
|
|
23
23
|
Requires-Dist: bazis-test-utils>=2.3.0; extra == "test"
|
|
@@ -236,7 +236,15 @@ This adds the endpoint: `GET /api/v1/async_background_response/{task_id}/`
|
|
|
236
236
|
|
|
237
237
|
### Project-Level Middleware
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
Add the middleware to the application (it is not added automatically):
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from bazis.contrib.async_request.middleware import AsyncRequestMiddleware
|
|
243
|
+
from bazis.core.app import app
|
|
244
|
+
|
|
245
|
+
app.add_middleware(AsyncRequestMiddleware)
|
|
246
|
+
```
|
|
247
|
+
|
|
240
248
|
Any request can be moved to background using the `X-Async-Background: true` header.
|
|
241
249
|
|
|
242
250
|
**Location**: `bazis.contrib.async_request.middleware.AsyncRequestMiddleware`
|
|
@@ -298,7 +306,9 @@ Runs 5 consumers in separate processes. Suitable for local development or deploy
|
|
|
298
306
|
|
|
299
307
|
**Parameters**:
|
|
300
308
|
|
|
301
|
-
- `--consumers-count` — number of consumers to run (default:
|
|
309
|
+
- `--consumers-count` — number of consumers to run (default: 15)
|
|
310
|
+
- `--restart-delay-sec` — delay before restarting a consumer that exited (default: 1.0)
|
|
311
|
+
- `--max-restarts` — restarts of a failed consumer before it is given up (default: unlimited)
|
|
302
312
|
|
|
303
313
|
## Working with Frontend
|
|
304
314
|
|
|
@@ -206,7 +206,15 @@ This adds the endpoint: `GET /api/v1/async_background_response/{task_id}/`
|
|
|
206
206
|
|
|
207
207
|
### Project-Level Middleware
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
Add the middleware to the application (it is not added automatically):
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from bazis.contrib.async_request.middleware import AsyncRequestMiddleware
|
|
213
|
+
from bazis.core.app import app
|
|
214
|
+
|
|
215
|
+
app.add_middleware(AsyncRequestMiddleware)
|
|
216
|
+
```
|
|
217
|
+
|
|
210
218
|
Any request can be moved to background using the `X-Async-Background: true` header.
|
|
211
219
|
|
|
212
220
|
**Location**: `bazis.contrib.async_request.middleware.AsyncRequestMiddleware`
|
|
@@ -268,7 +276,9 @@ Runs 5 consumers in separate processes. Suitable for local development or deploy
|
|
|
268
276
|
|
|
269
277
|
**Parameters**:
|
|
270
278
|
|
|
271
|
-
- `--consumers-count` — number of consumers to run (default:
|
|
279
|
+
- `--consumers-count` — number of consumers to run (default: 15)
|
|
280
|
+
- `--restart-delay-sec` — delay before restarting a consumer that exited (default: 1.0)
|
|
281
|
+
- `--max-restarts` — restarts of a failed consumer before it is given up (default: unlimited)
|
|
272
282
|
|
|
273
283
|
## Working with Frontend
|
|
274
284
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# bazis-async-request — guide for AI agents
|
|
2
|
+
|
|
3
|
+
Runs any HTTP request of a Bazis API in the background: a request with the header
|
|
4
|
+
`X-Async-Background` is queued in Kafka (bazis-async-background) and answered 202; a
|
|
5
|
+
consumer replays it through the ASGI application and stores the response for
|
|
6
|
+
`GET /async_background_response/{task_id}/`. Use it for slow endpoints and bulk changes.
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
BS_INSTALLED_APPS='[..., "bazis.contrib.async_request", "bazis.contrib.async_background"]'
|
|
12
|
+
BS_KAFKA_TASKS='["bazis.contrib.async_request.tasks"]' # the consumer of the requests
|
|
13
|
+
BS_KAFKA_BOOTSTRAP_SERVERS=kafka:9092
|
|
14
|
+
BS_KAFKA_TOPIC_ASYNC_BG=myproject_async_request
|
|
15
|
+
BS_KAFKA_GROUP_ID=myproject
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
# the main module of the application (as sample/sample/main.py)
|
|
20
|
+
from bazis.contrib.async_request.middleware import AsyncRequestMiddleware
|
|
21
|
+
from bazis.core.app import app
|
|
22
|
+
|
|
23
|
+
app.add_middleware(AsyncRequestMiddleware) # not added automatically
|
|
24
|
+
|
|
25
|
+
# the root router
|
|
26
|
+
router.register('bazis.contrib.async_background.router') # async_request.E001
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- Services and consumers as in bazis-async-background: Kafka, Redis, and
|
|
30
|
+
`python manage.py kafka_consumer_single` (or `kafka_consumer_multiple`) with the same
|
|
31
|
+
settings and database as the API.
|
|
32
|
+
- Without Kafka settings the middleware executes these requests synchronously and only
|
|
33
|
+
logs a warning (`async_request.W001`).
|
|
34
|
+
|
|
35
|
+
## Client
|
|
36
|
+
|
|
37
|
+
- Send the request as usual plus `X-Async-Background: true` (the header's presence is
|
|
38
|
+
enough) and `Authorization: Bearer <token>`: a session JWT, or an anonymous token of
|
|
39
|
+
bazis-ws (16–128 characters `A-Z a-z 0-9 _ -`). Without a valid token: 401. The request
|
|
40
|
+
is replayed with the same header: routes that read the user (bazis-users, permit, author)
|
|
41
|
+
answer 401 to an anonymous token, so use it only for routes that do not.
|
|
42
|
+
- Answer: 202 `{"data": null, "meta": {"async_request_id": <id>, "async_background_id": <id>}}`
|
|
43
|
+
(the same task id). Status updates arrive on the WebSocket channel of the token.
|
|
44
|
+
- Result: `GET .../async_background_response/{task_id}/` with the same token returns
|
|
45
|
+
`{"task_id", "endpoint", "status", "headers", "response"}` of the replayed request
|
|
46
|
+
(an HTTP error is a `completed` task with its status); `{"status": "not ready"}` before.
|
|
47
|
+
|
|
48
|
+
## Endpoints only for background requests
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fastapi import Depends
|
|
52
|
+
from bazis.contrib.async_request.utils import require_async
|
|
53
|
+
|
|
54
|
+
@router.post('/reports/generate/', dependencies=[Depends(require_async)])
|
|
55
|
+
async def generate_report(...): ...
|
|
56
|
+
|
|
57
|
+
class ShopRouteSet(JsonapiRouteBase):
|
|
58
|
+
@inject_make(CrudApiAction.UPDATE) # bazis.core.routes_abstract.initial
|
|
59
|
+
class InjectRequireAsync:
|
|
60
|
+
async_request: None = Depends(require_async) # the annotation is required
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A direct request to such an endpoint is 409.
|
|
64
|
+
|
|
65
|
+
## Rules
|
|
66
|
+
|
|
67
|
+
- Recognize the requests of the consumer only with `is_internal_request(scope)`
|
|
68
|
+
(`SCOPE_INTERNAL_KEY` in the ASGI scope, which clients cannot set). Never trust a
|
|
69
|
+
header: `X-Async-Background-Internal` is ignored.
|
|
70
|
+
- Only JSON bodies (an object or a list of objects) are queued: another body (multipart,
|
|
71
|
+
form, binary) is replayed as `{}`.
|
|
72
|
+
- The queued request keeps all headers, `Authorization` included, in the Kafka topic; it
|
|
73
|
+
runs with the client's token, so the token must still be valid when the consumer runs it.
|
|
74
|
+
- Requests with the same `data.id` in the body go to the same Kafka partition.
|
|
75
|
+
- WebSocket requests and the results route are never queued.
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/_version.py
RENAMED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 4, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g5ed041f2b'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# What the package is for and how to use it, for tools and AI agents (see AGENTS.md).
|
|
2
|
+
# `import` paths and `check` ids are verified by tests/test_manifest.py.
|
|
3
|
+
|
|
4
|
+
[package]
|
|
5
|
+
name = "bazis-async-request"
|
|
6
|
+
summary = "run any HTTP request of the API in the background through Kafka (X-Async-Background)"
|
|
7
|
+
solves = [
|
|
8
|
+
"move slow requests or bulk changes to the background without new endpoints",
|
|
9
|
+
"restrict an endpoint to requests executed in the background",
|
|
10
|
+
]
|
|
11
|
+
requires = ["bazis", "bazis-async-background"]
|
|
12
|
+
pairs_well = ["bazis-users", "bazis-permit"]
|
|
13
|
+
|
|
14
|
+
[[extension_points]]
|
|
15
|
+
name = "AsyncRequestMiddleware"
|
|
16
|
+
import = "bazis.contrib.async_request.middleware.AsyncRequestMiddleware"
|
|
17
|
+
use_when = "add to the application: app.add_middleware(AsyncRequestMiddleware)"
|
|
18
|
+
|
|
19
|
+
[[extension_points]]
|
|
20
|
+
name = "require_async"
|
|
21
|
+
import = "bazis.contrib.async_request.utils.require_async"
|
|
22
|
+
use_when = "a dependency that allows an endpoint only for background requests (409 otherwise)"
|
|
23
|
+
|
|
24
|
+
[[extension_points]]
|
|
25
|
+
name = "is_internal_request"
|
|
26
|
+
import = "bazis.contrib.async_request.utils.is_internal_request"
|
|
27
|
+
use_when = "whether a request (ASGI scope) is executed by the consumer"
|
|
28
|
+
|
|
29
|
+
[[extension_points]]
|
|
30
|
+
name = "consumer_async_requests"
|
|
31
|
+
import = "bazis.contrib.async_request.tasks.consumer_async_requests"
|
|
32
|
+
use_when = "the consumer of the requests: list bazis.contrib.async_request.tasks in KAFKA_TASKS"
|
|
33
|
+
|
|
34
|
+
[[pitfalls]]
|
|
35
|
+
text = "without Kafka settings the requests with X-Async-Background are executed synchronously"
|
|
36
|
+
check = "async_request.W001"
|
|
37
|
+
|
|
38
|
+
[[pitfalls]]
|
|
39
|
+
text = "without the route of bazis.contrib.async_background.router the middleware fails on every HTTP request"
|
|
40
|
+
check = "async_request.E001"
|
|
41
|
+
|
|
42
|
+
[[pitfalls]]
|
|
43
|
+
text = "the middleware is not added automatically: app.add_middleware(AsyncRequestMiddleware)"
|
|
44
|
+
|
|
45
|
+
[[pitfalls]]
|
|
46
|
+
text = "a header must never mark a request as internal: use is_internal_request(scope)"
|
|
47
|
+
|
|
48
|
+
[[pitfalls]]
|
|
49
|
+
text = "only JSON bodies are queued: multipart, form and binary bodies are replayed as {}"
|
|
50
|
+
|
|
51
|
+
[[pitfalls]]
|
|
52
|
+
text = "a dependency in an inject_make class without a type annotation is ignored"
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
"""
|
|
16
|
+
Django system checks of bazis-async-request (see `manage.py bazis_doctor`).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from django.conf import settings
|
|
20
|
+
from django.core.checks import Error, Warning, register
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@register()
|
|
24
|
+
def check_kafka_enabled(app_configs, **kwargs):
|
|
25
|
+
"""
|
|
26
|
+
Without Kafka the middleware executes the requests sent with `X-Async-Background`
|
|
27
|
+
synchronously, and only logs a warning.
|
|
28
|
+
"""
|
|
29
|
+
# the settings of bazis-async-background are missing if BS_BAZIS_APPS does not list it
|
|
30
|
+
if not getattr(settings, 'KAFKA_ENABLED', False):
|
|
31
|
+
return [
|
|
32
|
+
Warning(
|
|
33
|
+
'Kafka is not configured: the requests sent with X-Async-Background are '
|
|
34
|
+
'executed synchronously.',
|
|
35
|
+
hint=(
|
|
36
|
+
'Set BS_KAFKA_BOOTSTRAP_SERVERS and BS_KAFKA_TOPIC_ASYNC_BG (and list '
|
|
37
|
+
'bazis.contrib.async_background in BS_BAZIS_APPS if it is set).'
|
|
38
|
+
),
|
|
39
|
+
id='async_request.W001',
|
|
40
|
+
)
|
|
41
|
+
]
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@register()
|
|
46
|
+
def check_response_route(app_configs, **kwargs):
|
|
47
|
+
"""
|
|
48
|
+
The middleware looks up the route of the results to let its requests through: without
|
|
49
|
+
it every HTTP request through the middleware fails. Runs when the application is
|
|
50
|
+
loaded (`manage.py bazis_doctor`).
|
|
51
|
+
"""
|
|
52
|
+
from bazis.core.introspect import loaded_app
|
|
53
|
+
|
|
54
|
+
if (app := loaded_app()) is None:
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
from starlette.routing import NoMatchFound
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
app.url_path_for('get_async_background_response', task_id='__dummy__')
|
|
61
|
+
except NoMatchFound:
|
|
62
|
+
return [
|
|
63
|
+
Error(
|
|
64
|
+
'The route of the background results is not registered: '
|
|
65
|
+
'AsyncRequestMiddleware fails on every HTTP request.',
|
|
66
|
+
hint="Register it: router.register('bazis.contrib.async_background.router').",
|
|
67
|
+
id='async_request.E001',
|
|
68
|
+
)
|
|
69
|
+
]
|
|
70
|
+
return []
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/middleware.py
RENAMED
|
@@ -33,6 +33,16 @@ from .utils import build_request_payload, is_internal_request
|
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def partition_marker(body) -> str | None:
|
|
37
|
+
"""
|
|
38
|
+
The id of the JSON:API resource of the body: the requests of one resource are kept in
|
|
39
|
+
one Kafka partition, in order.
|
|
40
|
+
"""
|
|
41
|
+
data = body.get("data") if isinstance(body, dict) else None
|
|
42
|
+
item_id = data.get("id") if isinstance(data, dict) else None
|
|
43
|
+
return str(item_id) if item_id is not None else None
|
|
44
|
+
|
|
45
|
+
|
|
36
46
|
class AsyncRequestMiddleware:
|
|
37
47
|
def __init__(self, app):
|
|
38
48
|
self.app = app
|
|
@@ -88,9 +98,7 @@ class AsyncRequestMiddleware:
|
|
|
88
98
|
topic_name=settings.KAFKA_TOPIC_ASYNC_BG,
|
|
89
99
|
channel_name=channel_name,
|
|
90
100
|
payload=payload,
|
|
91
|
-
partition_marker=(
|
|
92
|
-
payload.body.get("data", {}).get("id") if isinstance(payload.body, dict) else None
|
|
93
|
-
),
|
|
101
|
+
partition_marker=partition_marker(payload.body),
|
|
94
102
|
)
|
|
95
103
|
|
|
96
104
|
response = JSONResponse(
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/utils.py
RENAMED
|
@@ -37,8 +37,13 @@ def build_request_payload(request: Request) -> AsyncRequestPayload:
|
|
|
37
37
|
"""Creates a payload for sending to Kafka."""
|
|
38
38
|
body_raw: bytes = request.scope.get("_cached_body") or getattr(request, "_body", b"")
|
|
39
39
|
try:
|
|
40
|
-
body
|
|
41
|
-
except
|
|
40
|
+
body = json.loads(body_raw.decode("utf-8")) if body_raw else {}
|
|
41
|
+
except ValueError:
|
|
42
|
+
# not JSON (multipart, binary): the body is not replayed
|
|
43
|
+
body = {}
|
|
44
|
+
if not isinstance(body, dict | list) or (
|
|
45
|
+
isinstance(body, list) and not all(isinstance(it, dict) for it in body)
|
|
46
|
+
):
|
|
42
47
|
body = {}
|
|
43
48
|
|
|
44
49
|
headers: list[tuple[str, str]] = []
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bazis-async-request
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Async Background Requests module for Bazis framework.
|
|
5
5
|
Author-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
6
6
|
Maintainer-email: Ilya Kharyn <ilya.tt07@gmail.com>
|
|
@@ -17,7 +17,7 @@ Classifier: Framework :: Django
|
|
|
17
17
|
Classifier: Framework :: FastAPI
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
|
-
Requires-Dist: bazis>=2.
|
|
20
|
+
Requires-Dist: bazis>=2.4.0
|
|
21
21
|
Requires-Dist: bazis-async-background>=2.3.0
|
|
22
22
|
Provides-Extra: test
|
|
23
23
|
Requires-Dist: bazis-test-utils>=2.3.0; extra == "test"
|
|
@@ -236,7 +236,15 @@ This adds the endpoint: `GET /api/v1/async_background_response/{task_id}/`
|
|
|
236
236
|
|
|
237
237
|
### Project-Level Middleware
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
Add the middleware to the application (it is not added automatically):
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from bazis.contrib.async_request.middleware import AsyncRequestMiddleware
|
|
243
|
+
from bazis.core.app import app
|
|
244
|
+
|
|
245
|
+
app.add_middleware(AsyncRequestMiddleware)
|
|
246
|
+
```
|
|
247
|
+
|
|
240
248
|
Any request can be moved to background using the `X-Async-Background: true` header.
|
|
241
249
|
|
|
242
250
|
**Location**: `bazis.contrib.async_request.middleware.AsyncRequestMiddleware`
|
|
@@ -298,7 +306,9 @@ Runs 5 consumers in separate processes. Suitable for local development or deploy
|
|
|
298
306
|
|
|
299
307
|
**Parameters**:
|
|
300
308
|
|
|
301
|
-
- `--consumers-count` — number of consumers to run (default:
|
|
309
|
+
- `--consumers-count` — number of consumers to run (default: 15)
|
|
310
|
+
- `--restart-delay-sec` — delay before restarting a consumer that exited (default: 1.0)
|
|
311
|
+
- `--max-restarts` — restarts of a failed consumer before it is given up (default: unlimited)
|
|
302
312
|
|
|
303
313
|
## Working with Frontend
|
|
304
314
|
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/SOURCES.txt
RENAMED
|
@@ -8,9 +8,12 @@ pytest.ini
|
|
|
8
8
|
.github/workflows/release.yml
|
|
9
9
|
.github/workflows/tests.yml
|
|
10
10
|
.github/workflows/update-licenses.yml
|
|
11
|
+
bazis/contrib/async_request/AGENTS.md
|
|
11
12
|
bazis/contrib/async_request/__init__.py
|
|
12
13
|
bazis/contrib/async_request/_version.py
|
|
13
14
|
bazis/contrib/async_request/apps.py
|
|
15
|
+
bazis/contrib/async_request/bazis_manifest.toml
|
|
16
|
+
bazis/contrib/async_request/checks.py
|
|
14
17
|
bazis/contrib/async_request/middleware.py
|
|
15
18
|
bazis/contrib/async_request/schemas.py
|
|
16
19
|
bazis/contrib/async_request/tasks.py
|
|
@@ -23,6 +26,7 @@ bazis_async_request.egg-info/scm_file_list.json
|
|
|
23
26
|
bazis_async_request.egg-info/scm_version.json
|
|
24
27
|
bazis_async_request.egg-info/top_level.txt
|
|
25
28
|
docs/releases/2.3.0.md
|
|
29
|
+
docs/releases/2.4.0.md
|
|
26
30
|
sample/Dockerfile
|
|
27
31
|
sample/docker-compose.kafka.yml
|
|
28
32
|
sample/docker-compose.test.yml
|
|
@@ -55,6 +59,7 @@ tests/__init__.py
|
|
|
55
59
|
tests/conftest.py
|
|
56
60
|
tests/test_decorator.py
|
|
57
61
|
tests/test_internal.py
|
|
62
|
+
tests/test_manifest.py
|
|
58
63
|
tests/test_multiple_order_requests.py
|
|
59
64
|
tests/test_order_access.py
|
|
60
65
|
tests/test_order_patch.py
|
|
@@ -8,13 +8,17 @@
|
|
|
8
8
|
".gitignore",
|
|
9
9
|
"CLAUDE.md",
|
|
10
10
|
"README.md",
|
|
11
|
+
"bazis/contrib/async_request/AGENTS.md",
|
|
11
12
|
"bazis/contrib/async_request/__init__.py",
|
|
12
13
|
"bazis/contrib/async_request/apps.py",
|
|
14
|
+
"bazis/contrib/async_request/bazis_manifest.toml",
|
|
15
|
+
"bazis/contrib/async_request/checks.py",
|
|
13
16
|
"bazis/contrib/async_request/middleware.py",
|
|
14
17
|
"bazis/contrib/async_request/schemas.py",
|
|
15
18
|
"bazis/contrib/async_request/tasks.py",
|
|
16
19
|
"bazis/contrib/async_request/utils.py",
|
|
17
20
|
"docs/releases/2.3.0.md",
|
|
21
|
+
"docs/releases/2.4.0.md",
|
|
18
22
|
"pyproject.toml",
|
|
19
23
|
"pytest.ini",
|
|
20
24
|
"sample/Dockerfile",
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
"tests/conftest.py",
|
|
50
54
|
"tests/test_decorator.py",
|
|
51
55
|
"tests/test_internal.py",
|
|
56
|
+
"tests/test_manifest.py",
|
|
52
57
|
"tests/test_multiple_order_requests.py",
|
|
53
58
|
"tests/test_order_access.py",
|
|
54
59
|
"tests/test_order_patch.py",
|
|
@@ -20,7 +20,7 @@ Requires Bazis 2.3.1 and bazis-async-background 2.3.
|
|
|
20
20
|
- **Consumers did not start with current FastStream** (`auto_commit` was removed): the
|
|
21
21
|
subscriber uses `subscriber_kwargs()` of bazis-async-background.
|
|
22
22
|
- The replayed request kept the `Content-Length` of the original request with a re-encoded
|
|
23
|
-
body and lacked the ASGI version and
|
|
23
|
+
body and lacked the ASGI version and the `state` key; `receive` no longer returns the
|
|
24
24
|
request again in a loop (streaming responses spun the CPU).
|
|
25
25
|
- The path of the replayed request was decoded twice (`%2541` became `A`) and the
|
|
26
26
|
`root_path` was lost: the payload now stores `path`, `raw_path` and `root_path` of the
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# bazis-async-request 2.4.0
|
|
2
|
+
|
|
3
|
+
Requires Bazis 2.4.
|
|
4
|
+
|
|
5
|
+
## Tools and AI agents
|
|
6
|
+
|
|
7
|
+
- The package ships `AGENTS.md` (setup, the client protocol, `require_async` and the
|
|
8
|
+
security rules) and `bazis_manifest.toml` for tools and AI agents (see `docs/agents.md`
|
|
9
|
+
of Bazis).
|
|
10
|
+
- New system checks (`manage.py bazis_doctor`):
|
|
11
|
+
- `async_request.W001`: Kafka is not configured, so the requests sent with
|
|
12
|
+
`X-Async-Background` are executed synchronously;
|
|
13
|
+
- `async_request.E001`: the route of the results (`bazis.contrib.async_background.router`)
|
|
14
|
+
is not registered, so `AsyncRequestMiddleware` fails on every HTTP request.
|
|
@@ -117,3 +117,50 @@ def test_payload_keeps_the_path_as_received():
|
|
|
117
117
|
assert payload.path == "/prefix/api/v1/file/%41 b/"
|
|
118
118
|
assert payload.raw_path == "/prefix/api/v1/file/%2541%20b/"
|
|
119
119
|
assert payload.root_path == "/prefix"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize('body', [b'\xff\xfe binary', b'"a string"', b'[1, 2]'])
|
|
123
|
+
def test_payload_of_a_body_that_is_not_a_resource(body):
|
|
124
|
+
"""
|
|
125
|
+
A binary body failed with UnicodeDecodeError and a JSON body that is not an object (or
|
|
126
|
+
a list of objects) failed the validation of the payload: 500.
|
|
127
|
+
"""
|
|
128
|
+
from starlette.requests import Request
|
|
129
|
+
|
|
130
|
+
from bazis.contrib.async_request.utils import build_request_payload
|
|
131
|
+
|
|
132
|
+
request = Request(
|
|
133
|
+
{
|
|
134
|
+
'type': 'http',
|
|
135
|
+
'method': 'POST',
|
|
136
|
+
'scheme': 'http',
|
|
137
|
+
'http_version': '1.1',
|
|
138
|
+
'path': '/api/v1/files/',
|
|
139
|
+
'raw_path': b'/api/v1/files/',
|
|
140
|
+
'query_string': b'',
|
|
141
|
+
'headers': [],
|
|
142
|
+
'client': ('127.0.0.1', 1),
|
|
143
|
+
'server': ('testserver', 80),
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
request._body = body
|
|
147
|
+
assert build_request_payload(request).body == {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.parametrize(
|
|
151
|
+
'body, marker',
|
|
152
|
+
[
|
|
153
|
+
({'data': {'id': 'a1', 'type': 'x.y'}}, 'a1'),
|
|
154
|
+
({'data': {'id': 7}}, '7'),
|
|
155
|
+
({'data': [{'id': 'a1'}]}, None),
|
|
156
|
+
({'data': None}, None),
|
|
157
|
+
([{'data': {'id': 'a1'}}], None),
|
|
158
|
+
],
|
|
159
|
+
)
|
|
160
|
+
def test_partition_marker(body, marker):
|
|
161
|
+
"""
|
|
162
|
+
The relationships bodies (`data` is a list or null) failed with AttributeError: 500.
|
|
163
|
+
"""
|
|
164
|
+
from bazis.contrib.async_request.middleware import partition_marker
|
|
165
|
+
|
|
166
|
+
assert partition_marker(body) == marker
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
from fastapi import FastAPI
|
|
16
|
+
|
|
17
|
+
from bazis.contrib.async_request.checks import check_kafka_enabled, check_response_route
|
|
18
|
+
from bazis.core.introspect import validate_manifest
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_manifest_is_valid():
|
|
22
|
+
assert validate_manifest('bazis.contrib.async_request') == []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_kafka_enabled_check(settings):
|
|
26
|
+
settings.KAFKA_ENABLED = False
|
|
27
|
+
assert [it.id for it in check_kafka_enabled(None)] == ['async_request.W001']
|
|
28
|
+
settings.KAFKA_ENABLED = True
|
|
29
|
+
assert check_kafka_enabled(None) == []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_response_route_check(sample_app, monkeypatch):
|
|
33
|
+
# the sample project registers bazis.contrib.async_background.router
|
|
34
|
+
assert check_response_route(None) == []
|
|
35
|
+
monkeypatch.setattr('bazis.core.introspect.loaded_app', lambda: FastAPI())
|
|
36
|
+
assert [it.id for it in check_response_route(None)] == ['async_request.E001']
|
|
File without changes
|
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/.github/workflows/update-licenses.yml
RENAMED
|
File without changes
|
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/__init__.py
RENAMED
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/schemas.py
RENAMED
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis/contrib/async_request/tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/fast_start/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/sample/users/migrations/0001_initial.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_multiple_order_requests.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazis_async_request-2.3.0 → bazis_async_request-2.4.0}/tests/test_shop_force_background_methods.py
RENAMED
|
File without changes
|
|
File without changes
|