p1-taskqueue 0.1.13__tar.gz → 0.1.15__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.
Potentially problematic release.
This version of p1-taskqueue might be problematic. Click here for more details.
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/PKG-INFO +1 -1
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/pyproject.toml +1 -1
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/PKG-INFO +1 -1
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/celery_app.py +40 -8
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/libs/helper_test.py +26 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_helper_test_functions.py +80 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_test_utils.py +101 -2
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/README.md +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/setup.cfg +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/SOURCES.txt +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/dependency_links.txt +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/requires.txt +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/top_level.txt +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/__init__.py +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/cmanager.py +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/libs/__init__.py +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/slack_notifier.py +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_celery_app.py +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_cmanager.py +0 -0
- {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_return_values.py +0 -0
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "p1-taskqueue"
|
|
7
7
|
# DO NOT CHANGE THIS VERSION - it gets automatically replaced by CI/CD with the git tag version
|
|
8
|
-
version = "0.1.
|
|
8
|
+
version = "0.1.15"
|
|
9
9
|
description = "A Task Queue Wrapper for Dekoruma Backend"
|
|
10
10
|
authors = [
|
|
11
11
|
{name = "Chalvin", email = "engineering@dekoruma.com"}
|
|
@@ -4,6 +4,7 @@ Reads configuration from Django settings and auto-configures queues with DLQ.
|
|
|
4
4
|
"""
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
|
+
from amqp.exceptions import PreconditionFailed
|
|
7
8
|
from celery import Celery
|
|
8
9
|
from kombu import Exchange
|
|
9
10
|
from kombu import Queue
|
|
@@ -67,6 +68,9 @@ def setup_queues(app, settings, celery_config):
|
|
|
67
68
|
queue_names = ['default', 'high', 'low']
|
|
68
69
|
dlq_name_prefix = getattr(settings, 'TASKQUEUE_DLQ_NAME_PREFIX', 'dlq')
|
|
69
70
|
|
|
71
|
+
logger.info(
|
|
72
|
+
f"[TaskQueue] Configuring app: {app_name}, queues: {queue_names}")
|
|
73
|
+
|
|
70
74
|
main_exchange = Exchange(app_name, type='direct')
|
|
71
75
|
dlx_exchange = Exchange(f'{app_name}.dlx', type='direct')
|
|
72
76
|
|
|
@@ -74,22 +78,28 @@ def setup_queues(app, settings, celery_config):
|
|
|
74
78
|
|
|
75
79
|
for queue_name in queue_names:
|
|
76
80
|
dlq_name = f'{dlq_name_prefix}.{queue_name}'
|
|
81
|
+
dlx_name = f'{app_name}.dlx'
|
|
82
|
+
|
|
83
|
+
queue_args = {
|
|
84
|
+
'x-dead-letter-exchange': dlx_name,
|
|
85
|
+
'x-dead-letter-routing-key': dlq_name
|
|
86
|
+
}
|
|
77
87
|
|
|
78
88
|
queue = Queue(
|
|
79
89
|
queue_name,
|
|
80
90
|
main_exchange,
|
|
81
91
|
routing_key=queue_name,
|
|
82
|
-
queue_arguments=
|
|
83
|
-
'x-dead-letter-exchange': f'{app_name}.dlx',
|
|
84
|
-
'x-dead-letter-routing-key': dlq_name
|
|
85
|
-
}
|
|
92
|
+
queue_arguments=queue_args
|
|
86
93
|
)
|
|
87
94
|
queues.append(queue)
|
|
95
|
+
logger.info(
|
|
96
|
+
f"[TaskQueue] Queue '{queue_name}' configured with DLX: {dlx_name}, DLQ routing key: {dlq_name}")
|
|
88
97
|
|
|
89
98
|
for queue_name in queue_names:
|
|
90
99
|
dlq_name = f'{dlq_name_prefix}.{queue_name}'
|
|
91
100
|
dlq = Queue(dlq_name, dlx_exchange, routing_key=dlq_name)
|
|
92
101
|
queues.append(dlq)
|
|
102
|
+
logger.info(f"[TaskQueue] DLQ '{dlq_name}' configured")
|
|
93
103
|
|
|
94
104
|
celery_config.update({
|
|
95
105
|
'task_default_queue': 'default',
|
|
@@ -100,14 +110,36 @@ def setup_queues(app, settings, celery_config):
|
|
|
100
110
|
|
|
101
111
|
try:
|
|
102
112
|
with app.connection_or_acquire() as conn:
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
channel = conn.default_channel
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
main_exchange.declare(channel=channel)
|
|
117
|
+
logger.info(f"[TaskQueue] Exchange declared: {app_name}")
|
|
118
|
+
except PreconditionFailed:
|
|
119
|
+
logger.info(f"[TaskQueue] Exchange already exists: {app_name}")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
dlx_exchange.declare(channel=channel)
|
|
123
|
+
logger.info(
|
|
124
|
+
f"[TaskQueue] DLX Exchange declared: {app_name}.dlx")
|
|
125
|
+
except PreconditionFailed:
|
|
126
|
+
logger.info(
|
|
127
|
+
f"[TaskQueue] DLX Exchange already exists: {app_name}.dlx")
|
|
105
128
|
|
|
106
129
|
for queue in queues:
|
|
107
|
-
|
|
130
|
+
try:
|
|
131
|
+
queue.declare(channel=channel)
|
|
132
|
+
logger.info(f"[TaskQueue] Queue declared: {queue.name}")
|
|
133
|
+
except PreconditionFailed:
|
|
134
|
+
logger.info(
|
|
135
|
+
f"[TaskQueue] Queue already exists with different config: {queue.name}. Using existing queue.")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"[TaskQueue] Failed to declare queue {queue.name}: {e}")
|
|
139
|
+
|
|
108
140
|
except Exception as e:
|
|
109
141
|
logger.warning(
|
|
110
|
-
f"[TaskQueue] Failed to
|
|
142
|
+
f"[TaskQueue] Failed to setup queues: {str(e.__class__.__name__)} {e}")
|
|
111
143
|
|
|
112
144
|
|
|
113
145
|
celery_app = create_celery_app()
|
|
@@ -70,6 +70,19 @@ def celery_worker_burst(include_func_names: List[str], channel: str = "default")
|
|
|
70
70
|
method_name = task_kwargs.get('method_name', '')
|
|
71
71
|
if module_path and class_name and method_name:
|
|
72
72
|
full_func_name = f"{module_path}.{class_name}.{method_name}"
|
|
73
|
+
elif task_name.endswith("callable_executor"):
|
|
74
|
+
callable_obj = task_kwargs.get('callable_obj')
|
|
75
|
+
if callable_obj:
|
|
76
|
+
module_path = getattr(
|
|
77
|
+
callable_obj, '__module__', '')
|
|
78
|
+
func_name = getattr(
|
|
79
|
+
callable_obj, '__name__', '')
|
|
80
|
+
if hasattr(callable_obj, '__self__'):
|
|
81
|
+
class_name = callable_obj.__self__.__class__.__name__
|
|
82
|
+
if module_path and class_name and func_name:
|
|
83
|
+
full_func_name = f"{module_path}.{class_name}.{func_name}"
|
|
84
|
+
elif module_path and func_name:
|
|
85
|
+
full_func_name = f"{module_path}.{func_name}"
|
|
73
86
|
|
|
74
87
|
should_execute = full_func_name in included_set if full_func_name else False
|
|
75
88
|
|
|
@@ -136,6 +149,19 @@ def get_queued_tasks(channel: str = "default"):
|
|
|
136
149
|
method_name = task_kwargs.get('method_name', '')
|
|
137
150
|
if module_path and class_name and method_name:
|
|
138
151
|
full_func_name = f"{module_path}.{class_name}.{method_name}"
|
|
152
|
+
elif task_name and task_name.endswith("callable_executor"):
|
|
153
|
+
callable_obj = task_kwargs.get('callable_obj')
|
|
154
|
+
if callable_obj:
|
|
155
|
+
module_path = getattr(
|
|
156
|
+
callable_obj, '__module__', '')
|
|
157
|
+
func_name = getattr(
|
|
158
|
+
callable_obj, '__name__', '')
|
|
159
|
+
if hasattr(callable_obj, '__self__'):
|
|
160
|
+
class_name = callable_obj.__self__.__class__.__name__
|
|
161
|
+
if module_path and class_name and func_name:
|
|
162
|
+
full_func_name = f"{module_path}.{class_name}.{func_name}"
|
|
163
|
+
elif module_path and func_name:
|
|
164
|
+
full_func_name = f"{module_path}.{func_name}"
|
|
139
165
|
|
|
140
166
|
queued_tasks.append({
|
|
141
167
|
'task_name': task_name,
|
|
@@ -140,6 +140,86 @@ class TestHelperTest:
|
|
|
140
140
|
assert result[0]['kwargs']['class_name'] == 'MyClass'
|
|
141
141
|
assert result[0]['kwargs']['method_name'] == 'my_method'
|
|
142
142
|
|
|
143
|
+
@patch('taskqueue.libs.helper_test.current_app')
|
|
144
|
+
@patch('taskqueue.libs.helper_test.loads')
|
|
145
|
+
def test_get_queued_tasks_given_callable_executor_function_expect_correct_parsing(self, mock_loads, mock_current_app):
|
|
146
|
+
# Use a real function instead of mocking
|
|
147
|
+
def my_function():
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
mock_message = MagicMock()
|
|
151
|
+
mock_message.headers = {
|
|
152
|
+
'task': 'taskqueue.cmanager.callable_executor'}
|
|
153
|
+
mock_message.body = b'mock_body'
|
|
154
|
+
mock_message.content_type = 'application/json'
|
|
155
|
+
mock_message.content_encoding = 'utf-8'
|
|
156
|
+
|
|
157
|
+
mock_loads.return_value = [[], {
|
|
158
|
+
'callable_obj': my_function,
|
|
159
|
+
'args': [],
|
|
160
|
+
'kwargs': {}
|
|
161
|
+
}]
|
|
162
|
+
|
|
163
|
+
mock_queue = MagicMock()
|
|
164
|
+
mock_queue.get.side_effect = [mock_message, None]
|
|
165
|
+
mock_current_app.amqp.queues = {
|
|
166
|
+
'default': MagicMock(return_value=mock_queue)}
|
|
167
|
+
|
|
168
|
+
mock_conn = MagicMock()
|
|
169
|
+
mock_chan = MagicMock()
|
|
170
|
+
mock_current_app.connection_for_read.return_value.__enter__.return_value = mock_conn
|
|
171
|
+
mock_conn.channel.return_value.__enter__.return_value = mock_chan
|
|
172
|
+
|
|
173
|
+
result = get_queued_tasks('default')
|
|
174
|
+
|
|
175
|
+
assert len(result) == 1
|
|
176
|
+
assert result[0]['task_name'] == 'taskqueue.cmanager.callable_executor'
|
|
177
|
+
assert result[0]['full_func_name'] == 'tests.test_helper_test_functions.my_function'
|
|
178
|
+
assert result[0]['args'] == []
|
|
179
|
+
assert result[0]['kwargs']['callable_obj'] == my_function
|
|
180
|
+
|
|
181
|
+
@patch('taskqueue.libs.helper_test.current_app')
|
|
182
|
+
@patch('taskqueue.libs.helper_test.loads')
|
|
183
|
+
def test_get_queued_tasks_given_callable_executor_method_expect_correct_parsing(self, mock_loads, mock_current_app):
|
|
184
|
+
# Use a real class and method instead of mocking
|
|
185
|
+
class MyClass:
|
|
186
|
+
def my_method(self):
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
instance = MyClass()
|
|
190
|
+
bound_method = instance.my_method
|
|
191
|
+
|
|
192
|
+
mock_message = MagicMock()
|
|
193
|
+
mock_message.headers = {
|
|
194
|
+
'task': 'taskqueue.cmanager.callable_executor'}
|
|
195
|
+
mock_message.body = b'mock_body'
|
|
196
|
+
mock_message.content_type = 'application/json'
|
|
197
|
+
mock_message.content_encoding = 'utf-8'
|
|
198
|
+
|
|
199
|
+
mock_loads.return_value = [[], {
|
|
200
|
+
'callable_obj': bound_method,
|
|
201
|
+
'args': [],
|
|
202
|
+
'kwargs': {}
|
|
203
|
+
}]
|
|
204
|
+
|
|
205
|
+
mock_queue = MagicMock()
|
|
206
|
+
mock_queue.get.side_effect = [mock_message, None]
|
|
207
|
+
mock_current_app.amqp.queues = {
|
|
208
|
+
'default': MagicMock(return_value=mock_queue)}
|
|
209
|
+
|
|
210
|
+
mock_conn = MagicMock()
|
|
211
|
+
mock_chan = MagicMock()
|
|
212
|
+
mock_current_app.connection_for_read.return_value.__enter__.return_value = mock_conn
|
|
213
|
+
mock_conn.channel.return_value.__enter__.return_value = mock_chan
|
|
214
|
+
|
|
215
|
+
result = get_queued_tasks('default')
|
|
216
|
+
|
|
217
|
+
assert len(result) == 1
|
|
218
|
+
assert result[0]['task_name'] == 'taskqueue.cmanager.callable_executor'
|
|
219
|
+
assert result[0]['full_func_name'] == 'tests.test_helper_test_functions.MyClass.my_method'
|
|
220
|
+
assert result[0]['args'] == []
|
|
221
|
+
assert result[0]['kwargs']['callable_obj'] == bound_method
|
|
222
|
+
|
|
143
223
|
@patch('taskqueue.libs.helper_test.get_queued_tasks')
|
|
144
224
|
def test_is_task_in_queue_given_task_exists_expect_true(self, mock_get_queued_tasks):
|
|
145
225
|
mock_get_queued_tasks.return_value = [
|
|
@@ -88,7 +88,8 @@ class TestCeleryWorkerBurst:
|
|
|
88
88
|
mock_message.ack.assert_called_once()
|
|
89
89
|
mock_task.apply.assert_called_once_with(
|
|
90
90
|
args=[],
|
|
91
|
-
kwargs={'module_path': 'module.submodule',
|
|
91
|
+
kwargs={'module_path': 'module.submodule',
|
|
92
|
+
'function_name': 'test_function', 'args': [], 'kwargs': {}}
|
|
92
93
|
)
|
|
93
94
|
|
|
94
95
|
@patch('taskqueue.libs.helper_test.current_app')
|
|
@@ -130,7 +131,8 @@ class TestCeleryWorkerBurst:
|
|
|
130
131
|
mock_message.ack.assert_called_once()
|
|
131
132
|
mock_task.apply.assert_called_once_with(
|
|
132
133
|
args=[],
|
|
133
|
-
kwargs={'module_path': 'module.submodule', 'class_name': 'TestClass',
|
|
134
|
+
kwargs={'module_path': 'module.submodule', 'class_name': 'TestClass',
|
|
135
|
+
'method_name': 'test_method', 'args': [], 'kwargs': {}, 'init_args': [], 'init_kwargs': {}}
|
|
134
136
|
)
|
|
135
137
|
|
|
136
138
|
@patch('taskqueue.libs.helper_test.current_app')
|
|
@@ -292,3 +294,100 @@ class TestCeleryWorkerBurst:
|
|
|
292
294
|
"Failed to process task taskqueue.cmanager.dynamic_function_executor: Exception: Task execution failed"
|
|
293
295
|
)
|
|
294
296
|
mock_message.ack.assert_called_once()
|
|
297
|
+
|
|
298
|
+
@patch('taskqueue.libs.helper_test.current_app')
|
|
299
|
+
@patch('taskqueue.libs.helper_test.loads')
|
|
300
|
+
def test_celery_worker_burst_given_callable_executor_function_expect_execution(self, mock_loads, mock_current_app):
|
|
301
|
+
def test_function():
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
mock_task = MagicMock()
|
|
305
|
+
mock_current_app.tasks = {
|
|
306
|
+
'taskqueue.cmanager.callable_executor': mock_task}
|
|
307
|
+
|
|
308
|
+
mock_message = MagicMock()
|
|
309
|
+
mock_message.headers = {
|
|
310
|
+
'task': 'taskqueue.cmanager.callable_executor'}
|
|
311
|
+
mock_message.body = b'mock_body'
|
|
312
|
+
mock_message.content_type = 'application/json'
|
|
313
|
+
mock_message.content_encoding = 'utf-8'
|
|
314
|
+
mock_message.acknowledged = False
|
|
315
|
+
|
|
316
|
+
mock_loads.return_value = [
|
|
317
|
+
[], {'callable_obj': test_function, 'args': [], 'kwargs': {}}]
|
|
318
|
+
|
|
319
|
+
mock_queue = MagicMock()
|
|
320
|
+
mock_queue.get.side_effect = [mock_message, None]
|
|
321
|
+
mock_queue_factory = MagicMock(return_value=mock_queue)
|
|
322
|
+
mock_current_app.amqp.queues = {'default': mock_queue_factory}
|
|
323
|
+
|
|
324
|
+
mock_conn = MagicMock()
|
|
325
|
+
mock_chan = MagicMock()
|
|
326
|
+
mock_current_app.connection_for_read.return_value.__enter__.return_value = mock_conn
|
|
327
|
+
mock_conn.channel.return_value.__enter__.return_value = mock_chan
|
|
328
|
+
|
|
329
|
+
with patch('taskqueue.libs.helper_test.logger') as mock_logger:
|
|
330
|
+
celery_worker_burst(
|
|
331
|
+
['tests.test_test_utils.test_function'])
|
|
332
|
+
|
|
333
|
+
mock_logger.info.assert_any_call(
|
|
334
|
+
"Executing task: tests.test_test_utils.test_function")
|
|
335
|
+
mock_logger.info.assert_any_call(
|
|
336
|
+
"Successfully executed task: tests.test_test_utils.test_function")
|
|
337
|
+
|
|
338
|
+
mock_message.ack.assert_called_once()
|
|
339
|
+
mock_task.apply.assert_called_once_with(
|
|
340
|
+
args=[],
|
|
341
|
+
kwargs={'callable_obj': test_function,
|
|
342
|
+
'args': [], 'kwargs': {}}
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
@patch('taskqueue.libs.helper_test.current_app')
|
|
346
|
+
@patch('taskqueue.libs.helper_test.loads')
|
|
347
|
+
def test_celery_worker_burst_given_callable_executor_method_expect_execution(self, mock_loads, mock_current_app):
|
|
348
|
+
class TestClass:
|
|
349
|
+
def test_method(self):
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
instance = TestClass()
|
|
353
|
+
bound_method = instance.test_method
|
|
354
|
+
|
|
355
|
+
mock_task = MagicMock()
|
|
356
|
+
mock_current_app.tasks = {
|
|
357
|
+
'taskqueue.cmanager.callable_executor': mock_task}
|
|
358
|
+
|
|
359
|
+
mock_message = MagicMock()
|
|
360
|
+
mock_message.headers = {
|
|
361
|
+
'task': 'taskqueue.cmanager.callable_executor'}
|
|
362
|
+
mock_message.body = b'mock_body'
|
|
363
|
+
mock_message.content_type = 'application/json'
|
|
364
|
+
mock_message.content_encoding = 'utf-8'
|
|
365
|
+
mock_message.acknowledged = False
|
|
366
|
+
|
|
367
|
+
mock_loads.return_value = [
|
|
368
|
+
[], {'callable_obj': bound_method, 'args': [], 'kwargs': {}}]
|
|
369
|
+
|
|
370
|
+
mock_queue = MagicMock()
|
|
371
|
+
mock_queue.get.side_effect = [mock_message, None]
|
|
372
|
+
mock_queue_factory = MagicMock(return_value=mock_queue)
|
|
373
|
+
mock_current_app.amqp.queues = {'default': mock_queue_factory}
|
|
374
|
+
|
|
375
|
+
mock_conn = MagicMock()
|
|
376
|
+
mock_chan = MagicMock()
|
|
377
|
+
mock_current_app.connection_for_read.return_value.__enter__.return_value = mock_conn
|
|
378
|
+
mock_conn.channel.return_value.__enter__.return_value = mock_chan
|
|
379
|
+
|
|
380
|
+
with patch('taskqueue.libs.helper_test.logger') as mock_logger:
|
|
381
|
+
celery_worker_burst(
|
|
382
|
+
['tests.test_test_utils.TestClass.test_method'])
|
|
383
|
+
|
|
384
|
+
mock_logger.info.assert_any_call(
|
|
385
|
+
"Executing task: tests.test_test_utils.TestClass.test_method")
|
|
386
|
+
mock_logger.info.assert_any_call(
|
|
387
|
+
"Successfully executed task: tests.test_test_utils.TestClass.test_method")
|
|
388
|
+
|
|
389
|
+
mock_message.ack.assert_called_once()
|
|
390
|
+
mock_task.apply.assert_called_once_with(
|
|
391
|
+
args=[],
|
|
392
|
+
kwargs={'callable_obj': bound_method, 'args': [], 'kwargs': {}}
|
|
393
|
+
)
|
|
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
|