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.

Files changed (20) hide show
  1. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/PKG-INFO +1 -1
  2. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/pyproject.toml +1 -1
  3. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/PKG-INFO +1 -1
  4. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/celery_app.py +40 -8
  5. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/libs/helper_test.py +26 -0
  6. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_helper_test_functions.py +80 -0
  7. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_test_utils.py +101 -2
  8. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/README.md +0 -0
  9. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/setup.cfg +0 -0
  10. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/SOURCES.txt +0 -0
  11. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/dependency_links.txt +0 -0
  12. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/requires.txt +0 -0
  13. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/p1_taskqueue.egg-info/top_level.txt +0 -0
  14. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/__init__.py +0 -0
  15. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/cmanager.py +0 -0
  16. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/libs/__init__.py +0 -0
  17. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/src/taskqueue/slack_notifier.py +0 -0
  18. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_celery_app.py +0 -0
  19. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_cmanager.py +0 -0
  20. {p1_taskqueue-0.1.13 → p1_taskqueue-0.1.15}/tests/test_return_values.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: p1-taskqueue
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: A Task Queue Wrapper for Dekoruma Backend
5
5
  Author-email: Chalvin <engineering@dekoruma.com>
6
6
  Project-URL: Homepage, https://github.com/Dekoruma/p1-taskqueue
@@ -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.13"
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"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: p1-taskqueue
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: A Task Queue Wrapper for Dekoruma Backend
5
5
  Author-email: Chalvin <engineering@dekoruma.com>
6
6
  Project-URL: Homepage, https://github.com/Dekoruma/p1-taskqueue
@@ -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
- main_exchange.declare(channel=conn.default_channel)
104
- dlx_exchange.declare(channel=conn.default_channel)
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
- queue.declare(channel=conn.default_channel)
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 declare queues: {str(e.__class__.__name__)} {e}")
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', 'function_name': 'test_function', 'args': [], 'kwargs': {}}
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', 'method_name': 'test_method', 'args': [], 'kwargs': {}, 'init_args': [], 'init_kwargs': {}}
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