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.
- bazis_async_request-2.4.0/.github/workflows/build-publish.yml +20 -0
- bazis_async_request-2.4.0/.github/workflows/release.yml +77 -0
- bazis_async_request-2.4.0/.github/workflows/tests.yml +131 -0
- bazis_async_request-2.4.0/CLAUDE.md +64 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/PKG-INFO +40 -22
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/README.md +32 -19
- bazis_async_request-2.4.0/bazis/contrib/async_request/AGENTS.md +75 -0
- bazis_async_request-2.4.0/bazis/contrib/async_request/_version.py +24 -0
- {bazis_async_request-2.2.3 → 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.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/middleware.py +13 -8
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/schemas.py +7 -5
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/tasks.py +38 -23
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/utils.py +28 -15
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/PKG-INFO +40 -22
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/SOURCES.txt +15 -1
- bazis_async_request-2.4.0/bazis_async_request.egg-info/requires.txt +12 -0
- bazis_async_request-2.4.0/bazis_async_request.egg-info/scm_file_list.json +65 -0
- bazis_async_request-2.4.0/bazis_async_request.egg-info/scm_version.json +8 -0
- bazis_async_request-2.4.0/docs/releases/2.3.0.md +52 -0
- bazis_async_request-2.4.0/docs/releases/2.4.0.md +14 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/pyproject.toml +8 -2
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/pytest.ini +2 -1
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/routes.py +1 -1
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/test.env +1 -1
- bazis_async_request-2.4.0/tests/__init__.py +14 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/conftest.py +36 -10
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_decorator.py +12 -8
- bazis_async_request-2.4.0/tests/test_internal.py +166 -0
- bazis_async_request-2.4.0/tests/test_manifest.py +36 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_multiple_order_requests.py +2 -2
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_order_access.py +7 -5
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_order_patch.py +12 -9
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_order_view.py +11 -7
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_shop_calc_and_params.py +6 -3
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/tests/test_shop_force_background_methods.py +2 -1
- bazis_async_request-2.4.0/tests/utils.py +35 -0
- bazis_async_request-2.2.3/.github/workflows/build-publish.yml +0 -16
- bazis_async_request-2.2.3/bazis/contrib/async_request/_version.py +0 -34
- bazis_async_request-2.2.3/bazis_async_request.egg-info/requires.txt +0 -7
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/.dockerignore +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/.github/workflows/update-licenses.yml +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/.gitignore +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis/contrib/async_request/__init__.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/dependency_links.txt +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/bazis_async_request.egg-info/top_level.txt +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/Dockerfile +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/docker-compose.kafka.yml +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/docker-compose.test.yml +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/__init__.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/admin.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/migrations/0001_initial.py +0 -0
- {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
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/migrations/__init__.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/models.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/router.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/fast_start/schemas.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/manage.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/project.env +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/pyproject.toml +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/__init__.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/main.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/router.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/settings.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/urls.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/sample/wsgi.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/__init__.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/admin.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/migrations/0001_initial.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/migrations/__init__.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/users/models.py +0 -0
- {bazis_async_request-2.2.3 → bazis_async_request-2.4.0}/sample/uv.lock +0 -0
- {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.
|
|
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
|
|
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.
|
|
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-
|
|
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-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
|
243
|
-
`X-Async-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
|
218
|
-
`X-Async-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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-
|
|
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-
|
|
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.
|