beanqueue 2.0.0rc0__tar.gz → 2.0.0rc1__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 (38) hide show
  1. beanqueue-2.0.0rc0/README.md → beanqueue-2.0.0rc1/PKG-INFO +65 -15
  2. beanqueue-2.0.0rc0/PKG-INFO → beanqueue-2.0.0rc1/README.md +45 -34
  3. beanqueue-2.0.0rc1/bq/app.py +508 -0
  4. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/cmds/create_tables.py +8 -1
  5. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/cmds/process.py +3 -1
  6. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/cmds/submit.py +20 -10
  7. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/config.py +63 -9
  8. beanqueue-2.0.0rc1/bq/db/session.py +6 -0
  9. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/metrics.py +25 -20
  10. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/processors/processor.py +63 -12
  11. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/processors/registry.py +7 -5
  12. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/processors/retry_policies.py +8 -5
  13. beanqueue-2.0.0rc1/bq/services/dispatch.py +135 -0
  14. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/services/worker.py +26 -24
  15. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/pyproject.toml +11 -4
  16. beanqueue-2.0.0rc0/bq/app.py +0 -484
  17. beanqueue-2.0.0rc0/bq/db/session.py +0 -5
  18. beanqueue-2.0.0rc0/bq/services/dispatch.py +0 -117
  19. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/.gitignore +0 -0
  20. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/LICENSE +0 -0
  21. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/__init__.py +0 -0
  22. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/cmds/__init__.py +0 -0
  23. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/cmds/cli.py +0 -0
  24. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/cmds/environment.py +0 -0
  25. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/cmds/main.py +0 -0
  26. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/cmds/utils.py +0 -0
  27. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/constants.py +0 -0
  28. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/db/__init__.py +0 -0
  29. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/db/base.py +0 -0
  30. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/events.py +0 -0
  31. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/models/__init__.py +0 -0
  32. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/models/event.py +0 -0
  33. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/models/helpers.py +0 -0
  34. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/models/task.py +0 -0
  35. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/models/worker.py +0 -0
  36. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/processors/__init__.py +0 -0
  37. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/services/__init__.py +0 -0
  38. {beanqueue-2.0.0rc0 → beanqueue-2.0.0rc1}/bq/utils.py +0 -0
@@ -1,6 +1,26 @@
1
+ Metadata-Version: 2.5
2
+ Name: beanqueue
3
+ Version: 2.0.0rc1
4
+ Summary: BeanQueue or BQ for short, PostgreSQL SKIP LOCK and SQLAlchemy based worker queue library
5
+ Author-email: Fang-Pen Lin <fangpen@launchplatform.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: <4,>=3.11
9
+ Requires-Dist: blinker<2,>=1.8.2
10
+ Requires-Dist: click<9,>=8.1.7
11
+ Requires-Dist: psycopg[binary]<4,>=3.1
12
+ Requires-Dist: pydantic-settings<3,>=2.2.1
13
+ Requires-Dist: rich<14,>=13.7.1
14
+ Requires-Dist: sqlalchemy[asyncio]<3,>=2.0.30
15
+ Requires-Dist: venusian<4,>=3.1.0
16
+ Provides-Extra: metrics
17
+ Requires-Dist: starlette<2,>=0.27; extra == 'metrics'
18
+ Requires-Dist: uvicorn<1,>=0.30.0; extra == 'metrics'
19
+ Description-Content-Type: text/markdown
20
+
1
21
  # BeanQueue [![CircleCI](https://dl.circleci.com/status-badge/img/gh/LaunchPlatform/bq/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/LaunchPlatform/bq/tree/master)
2
22
 
3
- BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https://www.sqlalchemy.org/), PostgreSQL [SKIP LOCKED queries](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/) and [NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) / [LISTEN](https://www.postgresql.org/docs/current/sql-listen.html) statements.
23
+ BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https://www.sqlalchemy.org/) 2.0 asyncio, [psycopg3](https://www.psycopg.org/psycopg3/), PostgreSQL [SKIP LOCKED queries](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/) and [NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) / [LISTEN](https://www.postgresql.org/docs/current/sql-listen.html) statements.
4
24
 
5
25
  **Notice**: Still in its early stage, we built this for [BeanHub](https://beanhub.io)'s internal usage. May change rapidly. Use at your own risk for now.
6
26
 
@@ -29,20 +49,39 @@ To enable the optional metrics HTTP server (currently `/healthz` only), install
29
49
  pip install "beanqueue[metrics]"
30
50
  ```
31
51
 
52
+ ## Testing
53
+
54
+ Unit and in-process acceptance tests:
55
+
56
+ ```bash
57
+ uv run python -m pytest tests
58
+ ```
59
+
60
+ End-to-end tests start PostgreSQL and three worker containers with Docker Compose, enqueue real tasks, apply load, kill a worker, and check graceful shutdown cleanup:
61
+
62
+ ```bash
63
+ uv run python -m pytest tests/e2e -svvvv
64
+ ```
65
+
32
66
  ## Upgrading to 2.0
33
67
 
34
- BeanQueue 2.0 includes breaking changes around the metrics HTTP server and custom health checks:
68
+ BeanQueue 2.0 is asyncio-first (SQLAlchemy `AsyncSession` + psycopg3) and includes breaking changes:
35
69
 
70
+ - **Workers use asyncio SQLAlchemy.** `BeanQueue.make_session()` returns an `AsyncSession`, `engine` is an `AsyncEngine`, and `process_tasks()` is `async`. Run it with `asyncio.run(app.process_tasks(...))` or `await` it from your own loop.
71
+ - **Database URLs use the psycopg3 driver.** Prefer `postgresql+psycopg://...`. Bare `postgresql://...` URLs are normalized to `postgresql+psycopg://...`.
72
+ - **`MAX_WORKER_THREADS` is now `MAX_CONCURRENT_TASKS`.** `BQ_MAX_WORKER_THREADS` and the old keyword still work as a deprecated alias.
73
+ - **Processors may be `async def` or `def`.** Async processors receive `db: AsyncSession`. Sync processors that take `db` still receive a sync `Session` via `AsyncSession.run_sync()`.
36
74
  - **`METRICS_HTTP_SERVER_ENABLED` defaults to `False`** (it was `True` in 1.x). Set `BQ_METRICS_HTTP_SERVER_ENABLED=true` to turn the server back on.
37
75
  - **The metrics server requires optional dependencies.** Install `beanqueue[metrics]` (`starlette` and `uvicorn`). Without them, enabling the server raises an error at startup.
38
- - **Custom health checks use the `healthz_check` event** (`bq.events.healthz_check`) instead of a `healthz_check` argument on `bq.BeanQueue`. Connect sync or async receivers to the signal.
76
+ - **Custom health checks use the `healthz_check` event** (`bq.events.healthz_check`) instead of a `healthz_check` argument on `bq.BeanQueue`. Receivers that query the database should use `AsyncSession` (`await session.execute(...)`).
39
77
 
40
78
  ## Usage
41
79
 
42
80
  You can define a basic task processor like this
43
81
 
44
82
  ```python
45
- from sqlalchemy.orm import Session
83
+ from sqlalchemy import select
84
+ from sqlalchemy.ext.asyncio import AsyncSession
46
85
 
47
86
  import bq
48
87
  from .. import models
@@ -50,15 +89,20 @@ from .. import image_utils
50
89
 
51
90
  app = bq.BeanQueue()
52
91
 
92
+
53
93
  @app.processor(channel="images")
54
- def resize_image(db: Session, task: bq.Task, width: int, height: int):
55
- image = db.query(models.Image).filter(models.Image.task == task).one()
94
+ async def resize_image(db: AsyncSession, task: bq.Task, width: int, height: int):
95
+ image = (
96
+ await db.execute(select(models.Image).where(models.Image.task == task))
97
+ ).scalar_one()
56
98
  image_utils.resize(image, size=(width, height))
57
99
  db.add(image)
58
100
  # by default the `processor` decorator has `auto_complete` flag turns on,
59
101
  # so it will commit the db changes for us automatically
60
102
  ```
61
103
 
104
+ Synchronous processors are still supported. If the function takes a `db` argument, BeanQueue injects the sync `Session` behind the `AsyncSession`. Processors that do not use the database can stay as plain `def` functions and are run in a worker thread so they do not block the event loop.
105
+
62
106
  The `db` and `task` keyword arguments are optional.
63
107
  If you don't need to access the task object, you can simply define the function without these two parameters.
64
108
  We also provide an optional `savepoint` argument in case if you want to rollback database changes you made.
@@ -152,13 +196,14 @@ To automatically retry a task after failure, you can specify a retry policy to t
152
196
  ```python
153
197
  import datetime
154
198
  import bq
155
- from sqlalchemy.orm import Session
199
+ from sqlalchemy.ext.asyncio import AsyncSession
156
200
 
157
201
  app = bq.BeanQueue()
158
202
  delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
159
203
 
204
+
160
205
  @app.processor(channel="images", retry_policy=delay_retry)
161
- def resize_image(db: Session, task: bq.Task, width: int, height: int):
206
+ async def resize_image(db: AsyncSession, task: bq.Task, width: int, height: int):
162
207
  # resize image here ...
163
208
  pass
164
209
  ```
@@ -178,8 +223,9 @@ To cap how many attempts are allowed, you can also use `LimitAttempt` like this:
178
223
  delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
179
224
  capped_delay_retry = bq.LimitAttempt(3, delay_retry)
180
225
 
226
+
181
227
  @app.processor(channel="images", retry_policy=capped_delay_retry)
182
- def resize_image(db: Session, task: bq.Task, width: int, height: int):
228
+ async def resize_image(db: AsyncSession, task: bq.Task, width: int, height: int):
183
229
  # Resize image here ...
184
230
  pass
185
231
  ```
@@ -192,7 +238,7 @@ You can also retry only for specific exception classes with the `retry_exception
192
238
  retry_policy=delay_retry,
193
239
  retry_exceptions=ValueError,
194
240
  )
195
- def resize_image(db: Session, task: bq.Task, width: int, height: int):
241
+ async def resize_image(db: AsyncSession, task: bq.Task, width: int, height: int):
196
242
  # resize image here ...
197
243
  pass
198
244
  ```
@@ -228,7 +274,9 @@ bq -a my_pkgs.bq.app process images
228
274
  Or if you prefer to define your own process command, you can also call `process_tasks` of the `BeanQueue` object directly like this:
229
275
 
230
276
  ```python
231
- app.process_tasks(channels=("images",))
277
+ import asyncio
278
+
279
+ asyncio.run(app.process_tasks(channels=("images",)))
232
280
  ```
233
281
 
234
282
  ### Health check and metrics HTTP server
@@ -294,7 +342,7 @@ Receivers may be synchronous or asynchronous; both can be mixed on the same sign
294
342
 
295
343
  ```python
296
344
  from sqlalchemy import text
297
- from sqlalchemy.orm import Session
345
+ from sqlalchemy.ext.asyncio import AsyncSession
298
346
 
299
347
  import bq
300
348
  from bq import events
@@ -303,12 +351,12 @@ app = bq.BeanQueue()
303
351
 
304
352
 
305
353
  @events.healthz_check.connect
306
- def check_database(sender: bq.BeanQueue, worker, session: Session):
307
- session.execute(text("SELECT 1"))
354
+ async def check_database(sender: bq.BeanQueue, worker, session: AsyncSession):
355
+ await session.execute(text("SELECT 1"))
308
356
 
309
357
 
310
358
  @events.healthz_check.connect
311
- async def check_external_service(sender: bq.BeanQueue, worker, session: Session):
359
+ async def check_external_service(sender: bq.BeanQueue, worker, session: AsyncSession):
312
360
  # async HTTP call, etc.
313
361
  ...
314
362
  ```
@@ -361,6 +409,7 @@ class Task(bq.TaskModelMixin, Base):
361
409
  "Worker", back_populates="tasks", uselist=False
362
410
  )
363
411
 
412
+
364
413
  listen_events(Task)
365
414
  ```
366
415
 
@@ -393,6 +442,7 @@ With the model class ready, you only need to change the `TASK_MODEL`, `WORKER_MO
393
442
 
394
443
  ```python
395
444
  import bq
445
+
396
446
  config = bq.Config(
397
447
  TASK_MODEL="my_pkgs.models.Task",
398
448
  WORKER_MODEL="my_pkgs.models.Worker",
@@ -1,25 +1,6 @@
1
- Metadata-Version: 2.4
2
- Name: beanqueue
3
- Version: 2.0.0rc0
4
- Summary: BeanQueue or BQ for short, PostgreSQL SKIP LOCK and SQLAlchemy based worker queue library
5
- Author-email: Fang-Pen Lin <fangpen@launchplatform.com>
6
- License-Expression: MIT
7
- License-File: LICENSE
8
- Requires-Python: <4,>=3.11
9
- Requires-Dist: blinker<2,>=1.8.2
10
- Requires-Dist: click<9,>=8.1.7
11
- Requires-Dist: pydantic-settings<3,>=2.2.1
12
- Requires-Dist: rich<14,>=13.7.1
13
- Requires-Dist: sqlalchemy<3,>=2.0.30
14
- Requires-Dist: venusian<4,>=3.1.0
15
- Provides-Extra: metrics
16
- Requires-Dist: starlette<2,>=0.27; extra == 'metrics'
17
- Requires-Dist: uvicorn<1,>=0.30.0; extra == 'metrics'
18
- Description-Content-Type: text/markdown
19
-
20
1
  # BeanQueue [![CircleCI](https://dl.circleci.com/status-badge/img/gh/LaunchPlatform/bq/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/LaunchPlatform/bq/tree/master)
21
2
 
22
- BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https://www.sqlalchemy.org/), PostgreSQL [SKIP LOCKED queries](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/) and [NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) / [LISTEN](https://www.postgresql.org/docs/current/sql-listen.html) statements.
3
+ BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https://www.sqlalchemy.org/) 2.0 asyncio, [psycopg3](https://www.psycopg.org/psycopg3/), PostgreSQL [SKIP LOCKED queries](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/) and [NOTIFY](https://www.postgresql.org/docs/current/sql-notify.html) / [LISTEN](https://www.postgresql.org/docs/current/sql-listen.html) statements.
23
4
 
24
5
  **Notice**: Still in its early stage, we built this for [BeanHub](https://beanhub.io)'s internal usage. May change rapidly. Use at your own risk for now.
25
6
 
@@ -48,20 +29,39 @@ To enable the optional metrics HTTP server (currently `/healthz` only), install
48
29
  pip install "beanqueue[metrics]"
49
30
  ```
50
31
 
32
+ ## Testing
33
+
34
+ Unit and in-process acceptance tests:
35
+
36
+ ```bash
37
+ uv run python -m pytest tests
38
+ ```
39
+
40
+ End-to-end tests start PostgreSQL and three worker containers with Docker Compose, enqueue real tasks, apply load, kill a worker, and check graceful shutdown cleanup:
41
+
42
+ ```bash
43
+ uv run python -m pytest tests/e2e -svvvv
44
+ ```
45
+
51
46
  ## Upgrading to 2.0
52
47
 
53
- BeanQueue 2.0 includes breaking changes around the metrics HTTP server and custom health checks:
48
+ BeanQueue 2.0 is asyncio-first (SQLAlchemy `AsyncSession` + psycopg3) and includes breaking changes:
54
49
 
50
+ - **Workers use asyncio SQLAlchemy.** `BeanQueue.make_session()` returns an `AsyncSession`, `engine` is an `AsyncEngine`, and `process_tasks()` is `async`. Run it with `asyncio.run(app.process_tasks(...))` or `await` it from your own loop.
51
+ - **Database URLs use the psycopg3 driver.** Prefer `postgresql+psycopg://...`. Bare `postgresql://...` URLs are normalized to `postgresql+psycopg://...`.
52
+ - **`MAX_WORKER_THREADS` is now `MAX_CONCURRENT_TASKS`.** `BQ_MAX_WORKER_THREADS` and the old keyword still work as a deprecated alias.
53
+ - **Processors may be `async def` or `def`.** Async processors receive `db: AsyncSession`. Sync processors that take `db` still receive a sync `Session` via `AsyncSession.run_sync()`.
55
54
  - **`METRICS_HTTP_SERVER_ENABLED` defaults to `False`** (it was `True` in 1.x). Set `BQ_METRICS_HTTP_SERVER_ENABLED=true` to turn the server back on.
56
55
  - **The metrics server requires optional dependencies.** Install `beanqueue[metrics]` (`starlette` and `uvicorn`). Without them, enabling the server raises an error at startup.
57
- - **Custom health checks use the `healthz_check` event** (`bq.events.healthz_check`) instead of a `healthz_check` argument on `bq.BeanQueue`. Connect sync or async receivers to the signal.
56
+ - **Custom health checks use the `healthz_check` event** (`bq.events.healthz_check`) instead of a `healthz_check` argument on `bq.BeanQueue`. Receivers that query the database should use `AsyncSession` (`await session.execute(...)`).
58
57
 
59
58
  ## Usage
60
59
 
61
60
  You can define a basic task processor like this
62
61
 
63
62
  ```python
64
- from sqlalchemy.orm import Session
63
+ from sqlalchemy import select
64
+ from sqlalchemy.ext.asyncio import AsyncSession
65
65
 
66
66
  import bq
67
67
  from .. import models
@@ -69,15 +69,20 @@ from .. import image_utils
69
69
 
70
70
  app = bq.BeanQueue()
71
71
 
72
+
72
73
  @app.processor(channel="images")
73
- def resize_image(db: Session, task: bq.Task, width: int, height: int):
74
- image = db.query(models.Image).filter(models.Image.task == task).one()
74
+ async def resize_image(db: AsyncSession, task: bq.Task, width: int, height: int):
75
+ image = (
76
+ await db.execute(select(models.Image).where(models.Image.task == task))
77
+ ).scalar_one()
75
78
  image_utils.resize(image, size=(width, height))
76
79
  db.add(image)
77
80
  # by default the `processor` decorator has `auto_complete` flag turns on,
78
81
  # so it will commit the db changes for us automatically
79
82
  ```
80
83
 
84
+ Synchronous processors are still supported. If the function takes a `db` argument, BeanQueue injects the sync `Session` behind the `AsyncSession`. Processors that do not use the database can stay as plain `def` functions and are run in a worker thread so they do not block the event loop.
85
+
81
86
  The `db` and `task` keyword arguments are optional.
82
87
  If you don't need to access the task object, you can simply define the function without these two parameters.
83
88
  We also provide an optional `savepoint` argument in case if you want to rollback database changes you made.
@@ -171,13 +176,14 @@ To automatically retry a task after failure, you can specify a retry policy to t
171
176
  ```python
172
177
  import datetime
173
178
  import bq
174
- from sqlalchemy.orm import Session
179
+ from sqlalchemy.ext.asyncio import AsyncSession
175
180
 
176
181
  app = bq.BeanQueue()
177
182
  delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
178
183
 
184
+
179
185
  @app.processor(channel="images", retry_policy=delay_retry)
180
- def resize_image(db: Session, task: bq.Task, width: int, height: int):
186
+ async def resize_image(db: AsyncSession, task: bq.Task, width: int, height: int):
181
187
  # resize image here ...
182
188
  pass
183
189
  ```
@@ -197,8 +203,9 @@ To cap how many attempts are allowed, you can also use `LimitAttempt` like this:
197
203
  delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
198
204
  capped_delay_retry = bq.LimitAttempt(3, delay_retry)
199
205
 
206
+
200
207
  @app.processor(channel="images", retry_policy=capped_delay_retry)
201
- def resize_image(db: Session, task: bq.Task, width: int, height: int):
208
+ async def resize_image(db: AsyncSession, task: bq.Task, width: int, height: int):
202
209
  # Resize image here ...
203
210
  pass
204
211
  ```
@@ -211,7 +218,7 @@ You can also retry only for specific exception classes with the `retry_exception
211
218
  retry_policy=delay_retry,
212
219
  retry_exceptions=ValueError,
213
220
  )
214
- def resize_image(db: Session, task: bq.Task, width: int, height: int):
221
+ async def resize_image(db: AsyncSession, task: bq.Task, width: int, height: int):
215
222
  # resize image here ...
216
223
  pass
217
224
  ```
@@ -247,7 +254,9 @@ bq -a my_pkgs.bq.app process images
247
254
  Or if you prefer to define your own process command, you can also call `process_tasks` of the `BeanQueue` object directly like this:
248
255
 
249
256
  ```python
250
- app.process_tasks(channels=("images",))
257
+ import asyncio
258
+
259
+ asyncio.run(app.process_tasks(channels=("images",)))
251
260
  ```
252
261
 
253
262
  ### Health check and metrics HTTP server
@@ -313,7 +322,7 @@ Receivers may be synchronous or asynchronous; both can be mixed on the same sign
313
322
 
314
323
  ```python
315
324
  from sqlalchemy import text
316
- from sqlalchemy.orm import Session
325
+ from sqlalchemy.ext.asyncio import AsyncSession
317
326
 
318
327
  import bq
319
328
  from bq import events
@@ -322,12 +331,12 @@ app = bq.BeanQueue()
322
331
 
323
332
 
324
333
  @events.healthz_check.connect
325
- def check_database(sender: bq.BeanQueue, worker, session: Session):
326
- session.execute(text("SELECT 1"))
334
+ async def check_database(sender: bq.BeanQueue, worker, session: AsyncSession):
335
+ await session.execute(text("SELECT 1"))
327
336
 
328
337
 
329
338
  @events.healthz_check.connect
330
- async def check_external_service(sender: bq.BeanQueue, worker, session: Session):
339
+ async def check_external_service(sender: bq.BeanQueue, worker, session: AsyncSession):
331
340
  # async HTTP call, etc.
332
341
  ...
333
342
  ```
@@ -380,6 +389,7 @@ class Task(bq.TaskModelMixin, Base):
380
389
  "Worker", back_populates="tasks", uselist=False
381
390
  )
382
391
 
392
+
383
393
  listen_events(Task)
384
394
  ```
385
395
 
@@ -412,6 +422,7 @@ With the model class ready, you only need to change the `TASK_MODEL`, `WORKER_MO
412
422
 
413
423
  ```python
414
424
  import bq
425
+
415
426
  config = bq.Config(
416
427
  TASK_MODEL="my_pkgs.models.Task",
417
428
  WORKER_MODEL="my_pkgs.models.Worker",