beanqueue 1.1.5__tar.gz → 1.1.7__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 (35) hide show
  1. beanqueue-1.1.7/.gitignore +161 -0
  2. {beanqueue-1.1.5 → beanqueue-1.1.7}/PKG-INFO +40 -47
  3. {beanqueue-1.1.5 → beanqueue-1.1.7}/README.md +28 -29
  4. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/app.py +17 -4
  5. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/config.py +14 -3
  6. beanqueue-1.1.7/pyproject.toml +35 -0
  7. beanqueue-1.1.5/pyproject.toml +0 -32
  8. {beanqueue-1.1.5 → beanqueue-1.1.7}/LICENSE +0 -0
  9. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/__init__.py +0 -0
  10. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/__init__.py +0 -0
  11. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/cli.py +0 -0
  12. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/create_tables.py +0 -0
  13. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/environment.py +0 -0
  14. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/main.py +0 -0
  15. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/process.py +0 -0
  16. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/submit.py +0 -0
  17. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/cmds/utils.py +0 -0
  18. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/constants.py +0 -0
  19. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/db/__init__.py +0 -0
  20. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/db/base.py +0 -0
  21. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/db/session.py +0 -0
  22. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/events.py +0 -0
  23. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/__init__.py +0 -0
  24. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/event.py +0 -0
  25. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/helpers.py +0 -0
  26. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/task.py +0 -0
  27. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/models/worker.py +0 -0
  28. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/processors/__init__.py +0 -0
  29. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/processors/processor.py +0 -0
  30. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/processors/registry.py +0 -0
  31. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/processors/retry_policies.py +0 -0
  32. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/services/__init__.py +0 -0
  33. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/services/dispatch.py +0 -0
  34. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/services/worker.py +0 -0
  35. {beanqueue-1.1.5 → beanqueue-1.1.7}/bq/utils.py +0 -0
@@ -0,0 +1,161 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
161
+ .idea
@@ -1,25 +1,21 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: beanqueue
3
- Version: 1.1.5
3
+ Version: 1.1.7
4
4
  Summary: BeanQueue or BQ for short, PostgreSQL SKIP LOCK and SQLAlchemy based worker queue library
5
- License: MIT
6
- Author: Fang-Pen Lin
7
- Author-email: fangpen@launchplatform.com
8
- Requires-Python: >=3.11,<4.0
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Requires-Dist: blinker (>=1.8.2,<2.0.0)
14
- Requires-Dist: click (>=8.1.7,<9.0.0)
15
- Requires-Dist: pg-activity (>=3.5.1,<4.0.0)
16
- Requires-Dist: pydantic-settings (>=2.2.1,<3.0.0)
17
- Requires-Dist: rich (>=13.7.1,<14.0.0)
18
- Requires-Dist: sqlalchemy (>=2.0.30,<3.0.0)
19
- Requires-Dist: venusian (>=3.1.0,<4.0.0)
5
+ Author-email: Fang-Pen Lin <fangpen@launchplatform.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: ~=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
20
15
  Description-Content-Type: text/markdown
21
16
 
22
- # 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/beanhub-extract/tree/master)
17
+ # 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/beanhub-extract/tree/master)
18
+
23
19
  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.
24
20
 
25
21
  **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.
@@ -27,14 +23,14 @@ BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https
27
23
  ## Features
28
24
 
29
25
  - **Super lightweight**: Under 1K lines
30
- - **Easy-to-deploy**: Only rely on PostgreSQL
31
- - **Easy-to-use**: Provide command line tools for processing tasks, also helpers for generating tasks models
32
- - **Auto-notify**: Notify will automatically be generated and send for inserted or update tasks
33
- - **Retry**: Built-in and customizable retry-policies
34
- - **Schedule**: Schedule task to run later
26
+ - **Easy-to-deploy**: Only relies on PostgreSQL
27
+ - **Transactional**: Commit your tasks with other database entries altogether without worrying about data inconsistencies
28
+ - **Easy-to-use**: Built-in command line tools for processing tasks and helpers for generating task models
29
+ - **Auto-notify**: Automatic generation of NOTIFY statements for new or updated tasks, ensuring fast task processing
30
+ - **Retry**: Built-in and customizable retry policies
31
+ - **Schedule**: Schedule tasks to run later
35
32
  - **Worker heartbeat and auto-reschedule**: Each worker keeps updating heartbeat, if one is found dead, the others will reschedule the tasks
36
- - **Customizable**: Use it as an library and build your own work queue
37
- - **Native DB operations**: Commit your tasks with other db entries altogether without worrying about data inconsistent issue
33
+ - **Customizable**: Custom Task, Worker and Event models. Use it as a library and build your own work queue
38
34
 
39
35
  ## Install
40
36
 
@@ -44,7 +40,7 @@ pip install beanqueue
44
40
 
45
41
  ## Usage
46
42
 
47
- You can define a task processor like this
43
+ You can define a basic task processor like this
48
44
 
49
45
  ```python
50
46
  from sqlalchemy.orm import Session
@@ -164,20 +160,20 @@ delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
164
160
 
165
161
  @app.processor(channel="images", retry_policy=delay_retry)
166
162
  def resize_image(db: Session, task: bq.Task, width: int, height: int):
167
- # resize iamge here ...
163
+ # resize image here ...
168
164
  pass
169
165
  ```
170
166
 
171
167
  Currently, we provide some simple common retry policies such as `DelayRetry` and `ExponentialBackoffRetry`.
172
- Surely, you can define your retry policy easily by making a function that returns an optional object at the next scheduled time for retry.
168
+ You can define your retry policy easily by making a function that returns an optional object at the next scheduled time for retry.
173
169
 
174
170
  ```python
175
171
  def my_retry_policy(task: bq.Task) -> typing.Any:
176
- # calculate delay based on task model ...
172
+ # Calculate delay based on task model ...
177
173
  return func.now() + datetime.timedelta(seconds=delay)
178
174
  ```
179
175
 
180
- To cap how many attempts is allowed, you can also use `LimitAttempt` like this:
176
+ To cap how many attempts are allowed, you can also use `LimitAttempt` like this:
181
177
 
182
178
  ```python
183
179
  delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
@@ -185,7 +181,7 @@ capped_delay_retry = bq.LimitAttempt(3, delay_retry)
185
181
 
186
182
  @app.processor(channel="images", retry_policy=capped_delay_retry)
187
183
  def resize_image(db: Session, task: bq.Task, width: int, height: int):
188
- # resize iamge here ...
184
+ # Resize image here ...
189
185
  pass
190
186
  ```
191
187
 
@@ -198,7 +194,7 @@ You can also retry only for specific exception classes with the `retry_exception
198
194
  retry_exceptions=ValueError,
199
195
  )
200
196
  def resize_image(db: Session, task: bq.Task, width: int, height: int):
201
- # resize iamge here ...
197
+ # resize image here ...
202
198
  pass
203
199
  ```
204
200
 
@@ -216,8 +212,6 @@ For example:
216
212
  import bq
217
213
  from .my_config import config
218
214
 
219
- container = bq.Container()
220
- container.wire(packages=[bq])
221
215
  config = bq.Config(
222
216
  PROCESSOR_PACKAGES=["my_pkgs.processors"],
223
217
  DATABASE_URL=config.DATABASE_URL,
@@ -241,9 +235,9 @@ app.process_tasks(channels=("images",))
241
235
  ### Define your own tables
242
236
 
243
237
  BeanQueue is designed to be as customizable as much as possible.
244
- Of course, you can define your own SQLAlchemy model instead of using the ones we provided.
238
+ One of its key features is that you can define your own SQLAlchemy model instead of using the ones we provided.
245
239
 
246
- To make defining your own `Task`, `Worker` or `Event` model much easier, you can use our mixin classes:
240
+ To make defining your own `Task`, `Worker` or `Event` model much easier, use bq's mixin classes:
247
241
 
248
242
  - `bq.TaskModelMixin`: provides task model columns
249
243
  - `bq.TaskModelRefWorkerMixin`: provides foreign key column and relationship to `bq.Worker`
@@ -259,12 +253,13 @@ Here's an example for defining your own Task model:
259
253
  ```python
260
254
  import uuid
261
255
 
262
- import bq
263
256
  from sqlalchemy import ForeignKey
264
257
  from sqlalchemy.dialects.postgresql import UUID
265
258
  from sqlalchemy.orm import Mapped
266
259
  from sqlalchemy.orm import mapped_column
267
260
  from sqlalchemy.orm import relationship
261
+ import bq
262
+ from bq.models.task import listen_events
268
263
 
269
264
  from .base_class import Base
270
265
 
@@ -281,15 +276,14 @@ class Task(bq.TaskModelMixin, Base):
281
276
  worker: Mapped["Worker"] = relationship(
282
277
  "Worker", back_populates="tasks", uselist=False
283
278
  )
284
- ```
285
279
 
286
- To make task insert and update with state changing to `PENDING` send out NOTIFY "channel" statement automatically, you can also use `bq.models.task.listen_events` helper to register our SQLAlchemy event handlers automatically like this
287
-
288
- ```python
289
- from bq.models.task import listen_events
290
280
  listen_events(Task)
291
281
  ```
292
282
 
283
+ For task insertion and updates to notify workers, we need to register any custom task types with `bq.models.task.listen_events`.
284
+ In the example above, this is done right after the Task model definition.
285
+ For more details and advanced usage, see the definition of `bq.models.task.listen_events`.
286
+
293
287
  You just see how easy it is to define your Task model. Now, here's an example for defining your own Worker model:
294
288
 
295
289
  ```python
@@ -362,7 +356,7 @@ With this approach, we don't need to worry about workers picking up the task too
362
356
  However, there's another drawback.
363
357
  If step 3 for pushing a new task to the work queue fails, the newly inserted `images` row will never be processed.
364
358
  There are many solutions to this problem, but these are all caused by inconsistent data views between the database and the work queue storage.
365
- Things will be much easier if we have a work queue that shares the same consistent view with the database.
359
+ Things would be much easier if we had a work queue that shared the same consistent view as the database.
366
360
 
367
361
  By using a database as the data storage, all the problems are gone.
368
362
  You can simply do the following:
@@ -383,10 +377,10 @@ However, things have changed since the [introduction of the SKIP LOCKED](https:/
383
377
  This project is inspired by many of the SKIP-LOCKED-based work queue successors.
384
378
  Why don't we just use those existing tools?
385
379
  Well, because while they work great as work queue solutions, they don't take advantage of writing tasks and their relative data into the database in a transaction.
386
- Many provide an abstraction function or gRPC method of pushing tasks into the database instead of opening it up for the user to insert the row directly with other rows and commit altogether.
380
+ Many provide an abstraction function or gRPC method for pushing tasks into the database, rather than allowing users to directly insert rows and commit them together.
387
381
 
388
- With BeanQueue, we don't abstract away the logic of publishing a new task into the queue.
389
- Instead, we open it up to let the user insert the row and choose when and what to commit to the task.
382
+ BeanQueue doesn't overly abstract the logic of publishing a new task into the queue.
383
+ Instead, you insert rows directly, choosing when and what to commit as tasks.
390
384
 
391
385
  ## Sponsor
392
386
 
@@ -406,4 +400,3 @@ A modern accounting book service based on the most popular open source version c
406
400
  - [PgQueuer](https://github.com/janbjorge/PgQueuer)
407
401
  - [hatchet](https://github.com/hatchet-dev/hatchet)
408
402
  - [procrastinate](https://github.com/procrastinate-org/procrastinate)
409
-
@@ -1,4 +1,5 @@
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/beanhub-extract/tree/master)
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/beanhub-extract/tree/master)
2
+
2
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.
3
4
 
4
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.
@@ -6,14 +7,14 @@ BeanQueue, a lightweight Python task queue framework based on [SQLAlchemy](https
6
7
  ## Features
7
8
 
8
9
  - **Super lightweight**: Under 1K lines
9
- - **Easy-to-deploy**: Only rely on PostgreSQL
10
- - **Easy-to-use**: Provide command line tools for processing tasks, also helpers for generating tasks models
11
- - **Auto-notify**: Notify will automatically be generated and send for inserted or update tasks
12
- - **Retry**: Built-in and customizable retry-policies
13
- - **Schedule**: Schedule task to run later
10
+ - **Easy-to-deploy**: Only relies on PostgreSQL
11
+ - **Transactional**: Commit your tasks with other database entries altogether without worrying about data inconsistencies
12
+ - **Easy-to-use**: Built-in command line tools for processing tasks and helpers for generating task models
13
+ - **Auto-notify**: Automatic generation of NOTIFY statements for new or updated tasks, ensuring fast task processing
14
+ - **Retry**: Built-in and customizable retry policies
15
+ - **Schedule**: Schedule tasks to run later
14
16
  - **Worker heartbeat and auto-reschedule**: Each worker keeps updating heartbeat, if one is found dead, the others will reschedule the tasks
15
- - **Customizable**: Use it as an library and build your own work queue
16
- - **Native DB operations**: Commit your tasks with other db entries altogether without worrying about data inconsistent issue
17
+ - **Customizable**: Custom Task, Worker and Event models. Use it as a library and build your own work queue
17
18
 
18
19
  ## Install
19
20
 
@@ -23,7 +24,7 @@ pip install beanqueue
23
24
 
24
25
  ## Usage
25
26
 
26
- You can define a task processor like this
27
+ You can define a basic task processor like this
27
28
 
28
29
  ```python
29
30
  from sqlalchemy.orm import Session
@@ -143,20 +144,20 @@ delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
143
144
 
144
145
  @app.processor(channel="images", retry_policy=delay_retry)
145
146
  def resize_image(db: Session, task: bq.Task, width: int, height: int):
146
- # resize iamge here ...
147
+ # resize image here ...
147
148
  pass
148
149
  ```
149
150
 
150
151
  Currently, we provide some simple common retry policies such as `DelayRetry` and `ExponentialBackoffRetry`.
151
- Surely, you can define your retry policy easily by making a function that returns an optional object at the next scheduled time for retry.
152
+ You can define your retry policy easily by making a function that returns an optional object at the next scheduled time for retry.
152
153
 
153
154
  ```python
154
155
  def my_retry_policy(task: bq.Task) -> typing.Any:
155
- # calculate delay based on task model ...
156
+ # Calculate delay based on task model ...
156
157
  return func.now() + datetime.timedelta(seconds=delay)
157
158
  ```
158
159
 
159
- To cap how many attempts is allowed, you can also use `LimitAttempt` like this:
160
+ To cap how many attempts are allowed, you can also use `LimitAttempt` like this:
160
161
 
161
162
  ```python
162
163
  delay_retry = bq.DelayRetry(delay=datetime.timedelta(seconds=120))
@@ -164,7 +165,7 @@ capped_delay_retry = bq.LimitAttempt(3, delay_retry)
164
165
 
165
166
  @app.processor(channel="images", retry_policy=capped_delay_retry)
166
167
  def resize_image(db: Session, task: bq.Task, width: int, height: int):
167
- # resize iamge here ...
168
+ # Resize image here ...
168
169
  pass
169
170
  ```
170
171
 
@@ -177,7 +178,7 @@ You can also retry only for specific exception classes with the `retry_exception
177
178
  retry_exceptions=ValueError,
178
179
  )
179
180
  def resize_image(db: Session, task: bq.Task, width: int, height: int):
180
- # resize iamge here ...
181
+ # resize image here ...
181
182
  pass
182
183
  ```
183
184
 
@@ -195,8 +196,6 @@ For example:
195
196
  import bq
196
197
  from .my_config import config
197
198
 
198
- container = bq.Container()
199
- container.wire(packages=[bq])
200
199
  config = bq.Config(
201
200
  PROCESSOR_PACKAGES=["my_pkgs.processors"],
202
201
  DATABASE_URL=config.DATABASE_URL,
@@ -220,9 +219,9 @@ app.process_tasks(channels=("images",))
220
219
  ### Define your own tables
221
220
 
222
221
  BeanQueue is designed to be as customizable as much as possible.
223
- Of course, you can define your own SQLAlchemy model instead of using the ones we provided.
222
+ One of its key features is that you can define your own SQLAlchemy model instead of using the ones we provided.
224
223
 
225
- To make defining your own `Task`, `Worker` or `Event` model much easier, you can use our mixin classes:
224
+ To make defining your own `Task`, `Worker` or `Event` model much easier, use bq's mixin classes:
226
225
 
227
226
  - `bq.TaskModelMixin`: provides task model columns
228
227
  - `bq.TaskModelRefWorkerMixin`: provides foreign key column and relationship to `bq.Worker`
@@ -238,12 +237,13 @@ Here's an example for defining your own Task model:
238
237
  ```python
239
238
  import uuid
240
239
 
241
- import bq
242
240
  from sqlalchemy import ForeignKey
243
241
  from sqlalchemy.dialects.postgresql import UUID
244
242
  from sqlalchemy.orm import Mapped
245
243
  from sqlalchemy.orm import mapped_column
246
244
  from sqlalchemy.orm import relationship
245
+ import bq
246
+ from bq.models.task import listen_events
247
247
 
248
248
  from .base_class import Base
249
249
 
@@ -260,15 +260,14 @@ class Task(bq.TaskModelMixin, Base):
260
260
  worker: Mapped["Worker"] = relationship(
261
261
  "Worker", back_populates="tasks", uselist=False
262
262
  )
263
- ```
264
263
 
265
- To make task insert and update with state changing to `PENDING` send out NOTIFY "channel" statement automatically, you can also use `bq.models.task.listen_events` helper to register our SQLAlchemy event handlers automatically like this
266
-
267
- ```python
268
- from bq.models.task import listen_events
269
264
  listen_events(Task)
270
265
  ```
271
266
 
267
+ For task insertion and updates to notify workers, we need to register any custom task types with `bq.models.task.listen_events`.
268
+ In the example above, this is done right after the Task model definition.
269
+ For more details and advanced usage, see the definition of `bq.models.task.listen_events`.
270
+
272
271
  You just see how easy it is to define your Task model. Now, here's an example for defining your own Worker model:
273
272
 
274
273
  ```python
@@ -341,7 +340,7 @@ With this approach, we don't need to worry about workers picking up the task too
341
340
  However, there's another drawback.
342
341
  If step 3 for pushing a new task to the work queue fails, the newly inserted `images` row will never be processed.
343
342
  There are many solutions to this problem, but these are all caused by inconsistent data views between the database and the work queue storage.
344
- Things will be much easier if we have a work queue that shares the same consistent view with the database.
343
+ Things would be much easier if we had a work queue that shared the same consistent view as the database.
345
344
 
346
345
  By using a database as the data storage, all the problems are gone.
347
346
  You can simply do the following:
@@ -362,10 +361,10 @@ However, things have changed since the [introduction of the SKIP LOCKED](https:/
362
361
  This project is inspired by many of the SKIP-LOCKED-based work queue successors.
363
362
  Why don't we just use those existing tools?
364
363
  Well, because while they work great as work queue solutions, they don't take advantage of writing tasks and their relative data into the database in a transaction.
365
- Many provide an abstraction function or gRPC method of pushing tasks into the database instead of opening it up for the user to insert the row directly with other rows and commit altogether.
364
+ Many provide an abstraction function or gRPC method for pushing tasks into the database, rather than allowing users to directly insert rows and commit them together.
366
365
 
367
- With BeanQueue, we don't abstract away the logic of publishing a new task into the queue.
368
- Instead, we open it up to let the user insert the row and choose when and what to commit to the task.
366
+ BeanQueue doesn't overly abstract the logic of publishing a new task into the queue.
367
+ Instead, you insert rows directly, choosing when and what to commit as tasks.
369
368
 
370
369
  ## Sponsor
371
370
 
@@ -5,7 +5,6 @@ import logging
5
5
  import platform
6
6
  import sys
7
7
  import threading
8
- import time
9
8
  import typing
10
9
  from importlib.metadata import PackageNotFoundError
11
10
  from importlib.metadata import version
@@ -63,6 +62,9 @@ class BeanQueue:
63
62
  self.worker_service_cls = worker_service_cls
64
63
  self.dispatch_service_cls = dispatch_service_cls
65
64
  self._engine = engine
65
+ self._worker_update_shutdown_event: threading.Event = threading.Event()
66
+ # noop if metrics thread is not started yet, shutdown if it is started
67
+ self._metrics_server_shutdown: typing.Callable[[], None] = lambda: None
66
68
 
67
69
  def create_default_engine(self):
68
70
  return create_engine(
@@ -184,7 +186,12 @@ class BeanQueue:
184
186
  )
185
187
  sys.exit(0)
186
188
 
187
- time.sleep(self.config.WORKER_HEARTBEAT_PERIOD)
189
+ do_shutdown = self._worker_update_shutdown_event.wait(
190
+ self.config.WORKER_HEARTBEAT_PERIOD
191
+ )
192
+ if do_shutdown:
193
+ return
194
+
188
195
  current_worker.last_heartbeat = func.now()
189
196
  db.add(current_worker)
190
197
  db.commit()
@@ -244,6 +251,8 @@ class BeanQueue:
244
251
  functools.partial(self._serve_http_request, worker_id),
245
252
  handler_class=WSGIRequestHandlerWithLogger,
246
253
  ) as httpd:
254
+ # expose graceful shutdown to the main thread
255
+ self._metrics_server_shutdown = httpd.shutdown
247
256
  logger.info("Run metrics HTTP server on %s:%s", host, port)
248
257
  httpd.serve_forever()
249
258
 
@@ -307,7 +316,7 @@ class BeanQueue:
307
316
  events.worker_init.send(self, worker=worker)
308
317
 
309
318
  logger.info("Processing tasks in channels = %s ...", channels)
310
-
319
+ # Graceful shutdown of worker update event on exit of the worker
311
320
  worker_update_thread = threading.Thread(
312
321
  target=functools.partial(
313
322
  self.update_workers,
@@ -357,9 +366,13 @@ class BeanQueue:
357
366
  except (SystemExit, KeyboardInterrupt):
358
367
  db.rollback()
359
368
  logger.info("Shutting down ...")
369
+ self._worker_update_shutdown_event.set()
360
370
  worker_update_thread.join(5)
361
371
  if metrics_server_thread is not None:
362
- metrics_server_thread.join(5)
372
+ # set a threading event, waits until server is shutdown
373
+ # serve the ongoing requests
374
+ self._metrics_server_shutdown()
375
+ metrics_server_thread.join(1)
363
376
 
364
377
  worker.state = models.WorkerState.SHUTDOWN
365
378
  db.add(worker)
@@ -1,5 +1,6 @@
1
1
  import typing
2
2
 
3
+ from pydantic import Field
3
4
  from pydantic import field_validator
4
5
  from pydantic import PostgresDsn
5
6
  from pydantic import ValidationInfo
@@ -10,7 +11,7 @@ from pydantic_settings import SettingsConfigDict
10
11
 
11
12
  class Config(BaseSettings):
12
13
  # Packages to scan for processor functions
13
- PROCESSOR_PACKAGES: list[str] = []
14
+ PROCESSOR_PACKAGES: list[str] = Field(default_factory=list)
14
15
 
15
16
  # Size of tasks batch to fetch each time from the database
16
17
  BATCH_SIZE: int = 1
@@ -58,8 +59,18 @@ class Config(BaseSettings):
58
59
  ) -> typing.Any:
59
60
  if isinstance(v, str):
60
61
  return v
61
- if isinstance(v, MultiHostUrl):
62
- return v
62
+ # Notice: Older Pydantic version (2.7), PostgresDsn is an annotated MultiHostUrl object,
63
+ # we cannot use isinstance with PostgresDsn directly. We need to check and see if PostgresDsn
64
+ # is an annotated type or not before we decide how to check if the passed in object is an
65
+ # PostgresDsn or not.
66
+ if typing.get_origin(PostgresDsn) is typing.Annotated:
67
+ if isinstance(v, MultiHostUrl):
68
+ return v
69
+ else:
70
+ if isinstance(v, PostgresDsn):
71
+ return v
72
+ if v is not None:
73
+ raise ValueError("Unexpected DATABASE_URL type")
63
74
  return PostgresDsn.build(
64
75
  scheme="postgresql",
65
76
  username=info.data.get("POSTGRES_USER"),
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "beanqueue"
3
+ version = "1.1.7"
4
+ description = "BeanQueue or BQ for short, PostgreSQL SKIP LOCK and SQLAlchemy based worker queue library"
5
+ authors = [{ name = "Fang-Pen Lin", email = "fangpen@launchplatform.com" }]
6
+ requires-python = "~=3.11"
7
+ readme = "README.md"
8
+ license = "MIT"
9
+ dependencies = [
10
+ "sqlalchemy>=2.0.30,<3",
11
+ "venusian>=3.1.0,<4",
12
+ "click>=8.1.7,<9",
13
+ "pydantic-settings>=2.2.1,<3",
14
+ "blinker>=1.8.2,<2",
15
+ "rich>=13.7.1,<14",
16
+ ]
17
+
18
+ [project.scripts]
19
+ bq = "bq.cmds.main:cli"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "psycopg2-binary>=2.9.9,<3",
24
+ "pytest-factoryboy>=2.7.0,<3",
25
+ ]
26
+
27
+ [tool.hatch.build.targets.sdist]
28
+ include = ["bq"]
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ include = ["bq"]
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
@@ -1,32 +0,0 @@
1
- [tool.poetry]
2
- name = "beanqueue"
3
- version = "1.1.5"
4
- description = "BeanQueue or BQ for short, PostgreSQL SKIP LOCK and SQLAlchemy based worker queue library"
5
- authors = ["Fang-Pen Lin <fangpen@launchplatform.com>"]
6
- license = "MIT"
7
- readme = "README.md"
8
- packages = [
9
- { include = "bq" },
10
- ]
11
-
12
- [tool.poetry.scripts]
13
- bq = "bq.cmds.main:cli"
14
-
15
- [tool.poetry.dependencies]
16
- python = "^3.11"
17
- sqlalchemy = "^2.0.30"
18
- venusian = "^3.1.0"
19
- click = "^8.1.7"
20
- pydantic-settings = "^2.2.1"
21
- pg-activity = "^3.5.1"
22
- blinker = "^1.8.2"
23
- rich = "^13.7.1"
24
-
25
-
26
- [tool.poetry.group.dev.dependencies]
27
- psycopg2-binary = "^2.9.9"
28
- pytest-factoryboy = "^2.7.0"
29
-
30
- [build-system]
31
- requires = ["poetry-core"]
32
- build-backend = "poetry.core.masonry.api"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes