bazis-async-request 2.2.3__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.
Files changed (74) hide show
  1. bazis_async_request-2.4.0/.github/workflows/build-publish.yml +20 -0
  2. bazis_async_request-2.4.0/.github/workflows/release.yml +77 -0
  3. bazis_async_request-2.4.0/.github/workflows/tests.yml +131 -0
  4. bazis_async_request-2.4.0/CLAUDE.md +64 -0
  5. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/PKG-INFO +40 -22
  6. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/README.md +32 -19
  7. bazis_async_request-2.4.0/bazis/contrib/async_request/AGENTS.md +75 -0
  8. bazis_async_request-2.4.0/bazis/contrib/async_request/_version.py +24 -0
  9. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/apps.py +5 -0
  10. bazis_async_request-2.4.0/bazis/contrib/async_request/bazis_manifest.toml +52 -0
  11. bazis_async_request-2.4.0/bazis/contrib/async_request/checks.py +70 -0
  12. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/middleware.py +13 -8
  13. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/schemas.py +7 -5
  14. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/tasks.py +38 -23
  15. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/utils.py +28 -15
  16. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/PKG-INFO +40 -22
  17. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/SOURCES.txt +15 -1
  18. bazis_async_request-2.4.0/bazis_async_request.egg-info/requires.txt +12 -0
  19. bazis_async_request-2.4.0/bazis_async_request.egg-info/scm_file_list.json +65 -0
  20. bazis_async_request-2.4.0/bazis_async_request.egg-info/scm_version.json +8 -0
  21. bazis_async_request-2.4.0/docs/releases/2.3.0.md +52 -0
  22. bazis_async_request-2.4.0/docs/releases/2.4.0.md +14 -0
  23. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/pyproject.toml +8 -2
  24. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/pytest.ini +2 -1
  25. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/routes.py +1 -1
  26. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/test.env +1 -1
  27. bazis_async_request-2.4.0/tests/__init__.py +14 -0
  28. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/conftest.py +36 -10
  29. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_decorator.py +12 -8
  30. bazis_async_request-2.4.0/tests/test_internal.py +166 -0
  31. bazis_async_request-2.4.0/tests/test_manifest.py +36 -0
  32. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_multiple_order_requests.py +2 -2
  33. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_order_access.py +7 -5
  34. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_order_patch.py +12 -9
  35. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_order_view.py +11 -7
  36. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_shop_calc_and_params.py +6 -3
  37. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_shop_force_background_methods.py +2 -1
  38. bazis_async_request-2.4.0/tests/utils.py +35 -0
  39. bazis_async_request-2.2.3/.github/workflows/build-publish.yml +0 -16
  40. bazis_async_request-2.2.3/bazis/contrib/async_request/_version.py +0 -34
  41. bazis_async_request-2.2.3/bazis_async_request.egg-info/requires.txt +0 -7
  42. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/.dockerignore +0 -0
  43. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/.github/workflows/update-licenses.yml +0 -0
  44. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/.gitignore +0 -0
  45. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/__init__.py +0 -0
  46. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/dependency_links.txt +0 -0
  47. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/top_level.txt +0 -0
  48. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/Dockerfile +0 -0
  49. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/docker-compose.kafka.yml +0 -0
  50. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/docker-compose.test.yml +0 -0
  51. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/__init__.py +0 -0
  52. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/admin.py +0 -0
  53. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/migrations/0001_initial.py +0 -0
  54. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/migrations/0002_remove_order_fast_start__autogen_8f29b0_gin_and_more.py +0 -0
  55. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/migrations/__init__.py +0 -0
  56. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/models.py +0 -0
  57. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/router.py +0 -0
  58. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/schemas.py +0 -0
  59. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/manage.py +0 -0
  60. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/project.env +0 -0
  61. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/pyproject.toml +0 -0
  62. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/__init__.py +0 -0
  63. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/main.py +0 -0
  64. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/router.py +0 -0
  65. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/settings.py +0 -0
  66. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/urls.py +0 -0
  67. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/wsgi.py +0 -0
  68. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/__init__.py +0 -0
  69. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/admin.py +0 -0
  70. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/migrations/0001_initial.py +0 -0
  71. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/migrations/__init__.py +0 -0
  72. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/models.py +0 -0
  73. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/uv.lock +0 -0
  74. {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/setup.cfg +0 -0
@@ -0,0 +1,20 @@
1
+ name: Build and Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+ # started by the Release workflow on the tag it creates: a tag pushed with the
8
+ # workflow token does not trigger the push event above
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: write
13
+
14
+ jobs:
15
+ build:
16
+ if: startsWith(github.ref, 'refs/tags/v')
17
+ uses: ecofuture-tech/.github/.github/workflows/reusable-build-publish.yml@main
18
+ secrets:
19
+ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
20
+ TEST_PYPI_TOKEN: ${{ secrets.TEST_PYPI_TOKEN }}
@@ -0,0 +1,77 @@
1
+ name: Release
2
+
3
+ # Creates the release tag vX.Y.Z on the current main commit and starts Build and Publish
4
+ # for it. Run it from the Actions tab (or through the GitHub API) on the main branch.
5
+
6
+ on:
7
+ workflow_dispatch:
8
+ inputs:
9
+ version:
10
+ description: 'Version without the "v" prefix, e.g. 2.3.0 or 2.4.0-rc1'
11
+ required: true
12
+ type: string
13
+
14
+ permissions:
15
+ contents: write
16
+ actions: write
17
+
18
+ concurrency:
19
+ group: release
20
+ cancel-in-progress: false
21
+
22
+ jobs:
23
+ release:
24
+ runs-on: ubuntu-latest
25
+ timeout-minutes: 10
26
+ if: github.ref == 'refs/heads/main'
27
+ env:
28
+ GH_TOKEN: ${{ github.token }}
29
+ VERSION: ${{ inputs.version }}
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ with:
33
+ fetch-depth: 0
34
+
35
+ - name: Validate the version
36
+ run: |
37
+ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)[0-9]+)?$ ]]; then
38
+ echo "::error::Invalid version '$VERSION', expected e.g. 2.3.0 or 2.4.0-rc1"
39
+ exit 1
40
+ fi
41
+ if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
42
+ echo "::error::Tag v$VERSION already exists"
43
+ exit 1
44
+ fi
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)
47
+ echo "Last release: ${LAST_TAG:-none}, new release: v$VERSION"
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
51
+ echo "::error::Version $VERSION is not newer than $LAST_TAG"
52
+ exit 1
53
+ fi
54
+
55
+ - name: Require green tests on this commit
56
+ run: |
57
+ CONCLUSION=$(gh run list --workflow tests.yml --commit "$GITHUB_SHA" --status completed \
58
+ --json conclusion --jq 'map(.conclusion) | first // "none"')
59
+ echo "Tests on $GITHUB_SHA: $CONCLUSION"
60
+ if [ "$CONCLUSION" != "success" ]; then
61
+ echo "::error::The Tests workflow has not succeeded on $GITHUB_SHA ($CONCLUSION)"
62
+ exit 1
63
+ fi
64
+
65
+ - name: Create the tag
66
+ run: |
67
+ git config user.name 'github-actions[bot]'
68
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
69
+ git tag -a "v$VERSION" -m "${{ github.event.repository.name }} $VERSION" "$GITHUB_SHA"
70
+ git push origin "v$VERSION"
71
+
72
+ - name: Start Build and Publish
73
+ run: gh workflow run build-publish.yml --ref "v$VERSION"
74
+
75
+ - name: Summary
76
+ run: |
77
+ echo "Tagged \`v$VERSION\` at \`$GITHUB_SHA\` and started Build and Publish." >> "$GITHUB_STEP_SUMMARY"
@@ -0,0 +1,131 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: tests-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ lint:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: astral-sh/setup-uv@v6
23
+ - run: uvx ruff check bazis tests sample
24
+
25
+ tests:
26
+ name: tests (python ${{ matrix.python }}, ${{ matrix.resolution }})
27
+ runs-on: ubuntu-latest
28
+ timeout-minutes: 30
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ python: ['3.12', '3.13', '3.14']
33
+ resolution: [highest]
34
+ include:
35
+ # the lowest versions allowed by pyproject.toml
36
+ - python: '3.12'
37
+ resolution: lowest-direct
38
+
39
+ services:
40
+ postgres:
41
+ image: postgis/postgis:16-3.4
42
+ env:
43
+ POSTGRES_USER: postgres
44
+ POSTGRES_PASSWORD: postgres
45
+ POSTGRES_DB: bazis
46
+ ports:
47
+ - 5432:5432
48
+ options: >-
49
+ --health-cmd pg_isready
50
+ --health-interval 5s
51
+ --health-timeout 5s
52
+ --health-retries 10
53
+ redis:
54
+ image: redis:7
55
+ ports:
56
+ - 6379:6379
57
+ options: >-
58
+ --health-cmd "redis-cli ping"
59
+ --health-interval 5s
60
+ --health-timeout 5s
61
+ --health-retries 10
62
+ kafka:
63
+ image: apache/kafka:3.9.1
64
+ ports:
65
+ - 9092:9092
66
+ options: >-
67
+ --health-cmd "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092"
68
+ --health-interval 10s
69
+ --health-timeout 10s
70
+ --health-retries 15
71
+ env:
72
+ BS_DEBUG: 'true'
73
+ BS_SECRET_KEY: ci-only-secret-key-that-is-long-enough-0123456789
74
+ BS_DATABASES__DEFAULT__HOST: localhost
75
+ BS_DATABASES__DEFAULT__PORT: '5432'
76
+ BS_DATABASES__DEFAULT__NAME: bazis
77
+ BS_DATABASES__DEFAULT__USER: postgres
78
+ BS_DATABASES__DEFAULT__PASSWORD: postgres
79
+ BS_CACHES__DEFAULT__LOCATION: redis://localhost:6379/1
80
+ BS_MEDIA_ROOT: /tmp/bazis/media
81
+ BS_STATIC_ROOT: /tmp/bazis/static
82
+ BS_WEBAPP_ROOT: /tmp/bazis/webapp
83
+ BS_KAFKA_BOOTSTRAP_SERVERS: localhost:9092
84
+ BS_KAFKA_TOPIC_ASYNC_BG: sample_local_async_request
85
+ BS_KAFKA_GROUP_ID: sample_local
86
+ BS_KAFKA_AUTO_OFFSET_RESET: earliest
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+
90
+ - name: Install GDAL (required by the PostGIS database backend)
91
+ # a stalled Ubuntu mirror must fail the step quickly instead of hanging the job
92
+ timeout-minutes: 10
93
+ run: |
94
+ APT="sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30"
95
+ $APT update
96
+ $APT install -y --no-install-recommends gdal-bin
97
+
98
+ - uses: astral-sh/setup-uv@v6
99
+ with:
100
+ python-version: ${{ matrix.python }}
101
+ activate-environment: true
102
+
103
+ - name: Install dependencies
104
+ run: uv pip install --resolution ${{ matrix.resolution }} -e '.[test]'
105
+
106
+ - name: Check for missing migrations
107
+ working-directory: sample
108
+ run: python manage.py makemigrations --check --dry-run
109
+
110
+ - name: Migrate the database shared by the tests and the consumer
111
+ # sample/settings.py sets TEST.NAME to the database name, so that the consumer sees
112
+ # the data of the tests; pytest runs with --reuse-db
113
+ working-directory: sample
114
+ run: |
115
+ python manage.py migrate -v0
116
+ python manage.py pgtrigger install
117
+
118
+ - name: Start a Kafka consumer
119
+ working-directory: sample
120
+ run: |
121
+ nohup python manage.py kafka_consumer_single > "$RUNNER_TEMP/consumer.log" 2>&1 &
122
+ sleep 15
123
+ cat "$RUNNER_TEMP/consumer.log"
124
+
125
+ - name: Run tests
126
+ working-directory: sample
127
+ run: python -m pytest ../tests -o addopts="--reuse-db" -p no:cacheprovider
128
+
129
+ - name: Consumer log
130
+ if: failure()
131
+ run: cat "$RUNNER_TEMP/consumer.log"
@@ -0,0 +1,64 @@
1
+ # bazis-async-request
2
+
3
+ Background HTTP requests for Bazis: `AsyncRequestMiddleware` puts a request with
4
+ `X-Async-Background: true` into Kafka (bazis-async-background) and answers 202 with the task
5
+ id; the consumer (`tasks.py`) replays the request through the ASGI application, marked as
6
+ internal in the ASGI scope (`SCOPE_INTERNAL_KEY`), and stores the response for
7
+ `GET /async_background_response/{task_id}/`. `require_async` limits an endpoint to such
8
+ requests.
9
+
10
+ Never trust a header to recognize the requests of the consumer: clients can send any header.
11
+
12
+ The package code is in `bazis/contrib/async_request`, the sample project used by the tests is in `sample/`,
13
+ the tests are in `tests/`.
14
+
15
+ ## Running the tests
16
+
17
+ The tests need PostgreSQL with PostGIS and Redis and Kafka (see `.github/workflows/tests.yml`).
18
+ Run them from the `sample` directory:
19
+
20
+ ```bash
21
+ cd sample
22
+ BS_DEBUG=true \
23
+ BS_SECRET_KEY=local-secret-key-that-is-long-enough-0123456789 \
24
+ BS_DATABASES__DEFAULT__HOST=localhost BS_DATABASES__DEFAULT__PORT=5432 \
25
+ BS_DATABASES__DEFAULT__NAME=bazis BS_DATABASES__DEFAULT__USER=postgres \
26
+ BS_DATABASES__DEFAULT__PASSWORD=postgres \
27
+ BS_CACHES__DEFAULT__LOCATION=redis://localhost:6379/1 \
28
+ BS_MEDIA_ROOT=/tmp/bazis/media BS_STATIC_ROOT=/tmp/bazis/static BS_WEBAPP_ROOT=/tmp/bazis/webapp \
29
+ BS_KAFKA_BOOTSTRAP_SERVERS=localhost:9092 BS_KAFKA_TOPIC_ASYNC_BG=sample_local_async_request \
30
+ BS_KAFKA_GROUP_ID=sample_local \
31
+ python -m pytest ../tests -o addopts="--reuse-db" -p no:cacheprovider
32
+ ```
33
+
34
+ The tests and the consumer share one database (`sample/sample/settings.py` sets `TEST.NAME`
35
+ to the database name): migrate it first (`python manage.py migrate` and
36
+ `python manage.py pgtrigger install` in `sample`), start a consumer with the same variables
37
+ (`python manage.py kafka_consumer_single &`) and run pytest with `-o addopts="--reuse-db"`.
38
+ The tests marked `run_with_consumer` are skipped without Kafka settings. A local Kafka
39
+ without Docker: download the Kafka binaries and start a single KRaft node.
40
+
41
+ Lint: `ruff check bazis tests sample`. CI also runs `python manage.py makemigrations --check
42
+ --dry-run` in `sample`: commit the migrations of model changes, including the sample apps.
43
+
44
+ ## Releasing
45
+
46
+ A release is the tag `vX.Y.Z` on `main`: the Build and Publish workflow builds the package
47
+ (the version comes from the tag through setuptools-scm) and publishes it to PyPI
48
+ (pre-releases `-alphaN`/`-betaN`/`-rcN` go to Test PyPI) and creates the GitHub release.
49
+
50
+ Claude Code sessions cannot push tags. Release through the **Release** workflow instead:
51
+
52
+ 1. Make sure the changes are merged into `main` and the Tests workflow is green on the
53
+ `main` head commit (the Release workflow checks this and refuses otherwise).
54
+ 2. Add the release notes as `docs/releases/X.Y.Z.md` in the change being released.
55
+ 3. Start the workflow `release.yml` on `ref: main` with the input `version: X.Y.Z`
56
+ (GitHub API: `POST /repos/ecofuture-tech/bazis-async-request/actions/workflows/release.yml/dispatches`;
57
+ with the GitHub MCP tools: `actions_run_trigger`, method `run_workflow`).
58
+ 4. The Release run creates the annotated tag and starts Build and Publish on it. Check
59
+ that both runs succeed and that the version appears on https://pypi.org/project/bazis-async-request/.
60
+
61
+ Release the Bazis packages in dependency order: a package is tested in CI against the
62
+ versions of its Bazis dependencies published on PyPI. Pick the version by semver:
63
+ breaking changes (settings renamed or required, dependency removed, behavior changed)
64
+ bump the minor version while the project is below 3.0.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazis-async-request
3
- Version: 2.2.3
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,9 +17,14 @@ 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-async-background
20
+ Requires-Dist: bazis>=2.4.0
21
+ Requires-Dist: bazis-async-background>=2.3.0
21
22
  Provides-Extra: test
22
- Requires-Dist: bazis-test-utils>=2.2.0; extra == "test"
23
+ Requires-Dist: bazis-test-utils>=2.3.0; extra == "test"
24
+ Requires-Dist: bazis-users>=2.3.0; extra == "test"
25
+ Requires-Dist: bazis-author>=2.3.0; extra == "test"
26
+ Requires-Dist: bazis-permit>=2.3.0; extra == "test"
27
+ Requires-Dist: bazis-ws>=2.3.0; extra == "test"
23
28
  Provides-Extra: dev
24
29
  Requires-Dist: ruff; extra == "dev"
25
30
 
@@ -83,7 +88,7 @@ python manage.py kafka_consumer_multiple --consumers-count=5
83
88
  - **WebSocket notifications** — automatic user notification about task status
84
89
  - **API endpoint** — retrieving results by task_id
85
90
 
86
- **How it works**: When sending a request with the `X-Async-Request: true` header, the request is not executed immediately but is placed in the Kafka queue. The consumer retrieves the task from the queue, executes it, saves the result in Redis, and sends a notification to the user via WebSocket.
91
+ **How it works**: When sending a request with the `X-Async-Background: true` header, the request is not executed immediately but is placed in the Kafka queue. The consumer retrieves the task from the queue, executes it, saves the result in Redis, and sends a notification to the user via WebSocket.
87
92
 
88
93
  **This package requires the installation of `bazis`, `bazis-users`, `bazis-ws` packages and running Kafka and Redis servers.**
89
94
 
@@ -127,7 +132,7 @@ This waits for the pytest container to finish and streams logs only from the Pyt
127
132
  ┌─────────────┐
128
133
  │ Client │
129
134
  └──────┬──────┘
130
- │ POST + X-Async-Request: true
135
+ │ POST + X-Async-Background: true
131
136
  ▼
132
137
  ┌─────────────────────┐
133
138
  │ API Endpoint │
@@ -231,24 +236,34 @@ This adds the endpoint: `GET /api/v1/async_background_response/{task_id}/`
231
236
 
232
237
  ### Project-Level Middleware
233
238
 
234
- AsyncRequestMiddleware is registered automatically when `bazis.contrib.async_request` is loaded.
235
- Any request can be moved to background using the `X-Async-Request: true` header.
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
+
248
+ Any request can be moved to background using the `X-Async-Background: true` header.
236
249
 
237
250
  **Location**: `bazis.contrib.async_request.middleware.AsyncRequestMiddleware`
238
251
 
239
252
  ### Endpoint-Only Async Request (Dependency)
240
253
 
241
254
  Use a dependency to mark specific endpoints as async-only. Such routes will return `409 Conflict`
242
- unless the request includes `X-Async-Request` (or an internal background call with
243
- `X-Async-Request-Internal: true`).
255
+ unless the request is executed in the background (the client sends it with
256
+ `X-Async-Background: true`, and the consumer executes it). The consumer marks its requests
257
+ in the ASGI scope, which clients cannot forge; the former `X-Async-Background-Internal`
258
+ header is ignored.
244
259
 
245
- **Location**: `bazis.contrib.async_request.dependencies.require_async`
260
+ **Location**: `bazis.contrib.async_request.utils.require_async`
246
261
 
247
262
  #### Attach to a Single Route
248
263
 
249
264
  ```python
250
265
  from fastapi import Depends
251
- from bazis.contrib.async_request.dependencies import require_async
266
+ from bazis.contrib.async_request.utils import require_async
252
267
 
253
268
  @router.post(
254
269
  "/reports/generate/",
@@ -262,12 +277,12 @@ async def generate_report(...):
262
277
 
263
278
  ```python
264
279
  from fastapi import Depends
265
- from bazis.contrib.async_request.dependencies import require_async
280
+ from bazis.contrib.async_request.utils import require_async
266
281
 
267
282
  @router.post("/reports/generate/")
268
283
  async def generate_report(
269
284
  ...,
270
- _async_request: None = Depends(require_async),
285
+ async_request: None = Depends(require_async),
271
286
  ):
272
287
  ...
273
288
  ```
@@ -291,20 +306,22 @@ Runs 5 consumers in separate processes. Suitable for local development or deploy
291
306
 
292
307
  **Parameters**:
293
308
 
294
- - `--consumers-count` — number of consumers to run (default: 1)
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)
295
312
 
296
313
  ## Working with Frontend
297
314
 
298
315
  ### Sending a Request
299
316
 
300
- Add the `X-Async-Request: true` header to your request:
317
+ Add the `X-Async-Background: true` header to your request:
301
318
 
302
319
  ```bash
303
320
  curl -X POST \
304
321
  http://localhost/api/v1/orders/order/ \
305
322
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
306
323
  -H "Content-Type: application/vnd.api+json" \
307
- -H "X-Async-Request: true" \
324
+ -H "X-Async-Background: true" \
308
325
  -d '{
309
326
  "data": {
310
327
  "type": "myapp.order",
@@ -327,11 +344,12 @@ curl -X POST \
327
344
  }
328
345
  ```
329
346
 
330
- If the request has no `Authorization` header, pass a channel name directly:
347
+ A client without a user token sends an anonymous token it generated itself (16–128
348
+ characters `A-Z a-z 0-9 _ -`, e.g. a random UUID) as `Authorization: Bearer <token>`; the same
349
+ token subscribes to its WebSocket channel (bazis-ws) and reads the result.
331
350
 
332
- ```bash
333
- X-Async-Request: <channel_name>
334
- ```
351
+ The request is stored in the Kafka topic with all its headers, `Authorization` included, and
352
+ executed later with them: protect the topic accordingly.
335
353
 
336
354
  Save the `async_request_id` — this is the task identifier for retrieving the result.
337
355
 
@@ -503,7 +521,7 @@ class AsyncReportClient {
503
521
  headers: {
504
522
  'Authorization': `Bearer ${this.token}`,
505
523
  'Content-Type': 'application/vnd.api+json',
506
- 'X-Async-Request': 'true'
524
+ 'X-Async-Background': 'true'
507
525
  },
508
526
  body: JSON.stringify({
509
527
  data: {
@@ -633,7 +651,7 @@ Content-Type: application/json
633
651
  # Asynchronous request (returns task_id immediately)
634
652
  POST /api/v1/generate-analytics/
635
653
  Content-Type: application/json
636
- X-Async-Request: true
654
+ X-Async-Background: true
637
655
 
638
656
  {
639
657
  "report_type": "sales",
@@ -58,7 +58,7 @@ python manage.py kafka_consumer_multiple --consumers-count=5
58
58
  - **WebSocket notifications** — automatic user notification about task status
59
59
  - **API endpoint** — retrieving results by task_id
60
60
 
61
- **How it works**: When sending a request with the `X-Async-Request: true` header, the request is not executed immediately but is placed in the Kafka queue. The consumer retrieves the task from the queue, executes it, saves the result in Redis, and sends a notification to the user via WebSocket.
61
+ **How it works**: When sending a request with the `X-Async-Background: true` header, the request is not executed immediately but is placed in the Kafka queue. The consumer retrieves the task from the queue, executes it, saves the result in Redis, and sends a notification to the user via WebSocket.
62
62
 
63
63
  **This package requires the installation of `bazis`, `bazis-users`, `bazis-ws` packages and running Kafka and Redis servers.**
64
64
 
@@ -102,7 +102,7 @@ This waits for the pytest container to finish and streams logs only from the Pyt
102
102
  ┌─────────────┐
103
103
  │ Client │
104
104
  └──────┬──────┘
105
- │ POST + X-Async-Request: true
105
+ │ POST + X-Async-Background: true
106
106
  ▼
107
107
  ┌─────────────────────┐
108
108
  │ API Endpoint │
@@ -206,24 +206,34 @@ This adds the endpoint: `GET /api/v1/async_background_response/{task_id}/`
206
206
 
207
207
  ### Project-Level Middleware
208
208
 
209
- AsyncRequestMiddleware is registered automatically when `bazis.contrib.async_request` is loaded.
210
- Any request can be moved to background using the `X-Async-Request: true` header.
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
+
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`
213
221
 
214
222
  ### Endpoint-Only Async Request (Dependency)
215
223
 
216
224
  Use a dependency to mark specific endpoints as async-only. Such routes will return `409 Conflict`
217
- unless the request includes `X-Async-Request` (or an internal background call with
218
- `X-Async-Request-Internal: true`).
225
+ unless the request is executed in the background (the client sends it with
226
+ `X-Async-Background: true`, and the consumer executes it). The consumer marks its requests
227
+ in the ASGI scope, which clients cannot forge; the former `X-Async-Background-Internal`
228
+ header is ignored.
219
229
 
220
- **Location**: `bazis.contrib.async_request.dependencies.require_async`
230
+ **Location**: `bazis.contrib.async_request.utils.require_async`
221
231
 
222
232
  #### Attach to a Single Route
223
233
 
224
234
  ```python
225
235
  from fastapi import Depends
226
- from bazis.contrib.async_request.dependencies import require_async
236
+ from bazis.contrib.async_request.utils import require_async
227
237
 
228
238
  @router.post(
229
239
  "/reports/generate/",
@@ -237,12 +247,12 @@ async def generate_report(...):
237
247
 
238
248
  ```python
239
249
  from fastapi import Depends
240
- from bazis.contrib.async_request.dependencies import require_async
250
+ from bazis.contrib.async_request.utils import require_async
241
251
 
242
252
  @router.post("/reports/generate/")
243
253
  async def generate_report(
244
254
  ...,
245
- _async_request: None = Depends(require_async),
255
+ async_request: None = Depends(require_async),
246
256
  ):
247
257
  ...
248
258
  ```
@@ -266,20 +276,22 @@ Runs 5 consumers in separate processes. Suitable for local development or deploy
266
276
 
267
277
  **Parameters**:
268
278
 
269
- - `--consumers-count` — number of consumers to run (default: 1)
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)
270
282
 
271
283
  ## Working with Frontend
272
284
 
273
285
  ### Sending a Request
274
286
 
275
- Add the `X-Async-Request: true` header to your request:
287
+ Add the `X-Async-Background: true` header to your request:
276
288
 
277
289
  ```bash
278
290
  curl -X POST \
279
291
  http://localhost/api/v1/orders/order/ \
280
292
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
281
293
  -H "Content-Type: application/vnd.api+json" \
282
- -H "X-Async-Request: true" \
294
+ -H "X-Async-Background: true" \
283
295
  -d '{
284
296
  "data": {
285
297
  "type": "myapp.order",
@@ -302,11 +314,12 @@ curl -X POST \
302
314
  }
303
315
  ```
304
316
 
305
- If the request has no `Authorization` header, pass a channel name directly:
317
+ A client without a user token sends an anonymous token it generated itself (16–128
318
+ characters `A-Z a-z 0-9 _ -`, e.g. a random UUID) as `Authorization: Bearer <token>`; the same
319
+ token subscribes to its WebSocket channel (bazis-ws) and reads the result.
306
320
 
307
- ```bash
308
- X-Async-Request: <channel_name>
309
- ```
321
+ The request is stored in the Kafka topic with all its headers, `Authorization` included, and
322
+ executed later with them: protect the topic accordingly.
310
323
 
311
324
  Save the `async_request_id` — this is the task identifier for retrieving the result.
312
325
 
@@ -478,7 +491,7 @@ class AsyncReportClient {
478
491
  headers: {
479
492
  'Authorization': `Bearer ${this.token}`,
480
493
  'Content-Type': 'application/vnd.api+json',
481
- 'X-Async-Request': 'true'
494
+ 'X-Async-Background': 'true'
482
495
  },
483
496
  body: JSON.stringify({
484
497
  data: {
@@ -608,7 +621,7 @@ Content-Type: application/json
608
621
  # Asynchronous request (returns task_id immediately)
609
622
  POST /api/v1/generate-analytics/
610
623
  Content-Type: application/json
611
- X-Async-Request: true
624
+ X-Async-Background: true
612
625
 
613
626
  {
614
627
  "report_type": "sales",
@@ -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.