ergon-framework-python 0.1.0__py3-none-any.whl

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 (82) hide show
  1. ergon/__init__.py +13 -0
  2. ergon/bootstrap/src/__project__/__init__.py +0 -0
  3. ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +124 -0
  4. ergon/bootstrap/src/__project__/_observability/grafana.yaml +17 -0
  5. ergon/bootstrap/src/__project__/_observability/loki.yaml +48 -0
  6. ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +53 -0
  7. ergon/bootstrap/src/__project__/_observability/prometheus.yaml +11 -0
  8. ergon/bootstrap/src/__project__/_observability/tempo.yaml +24 -0
  9. ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
  10. ergon/bootstrap/src/__project__/main.py +9 -0
  11. ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
  12. ergon/bootstrap/src/__project__/tasks/constants.py +13 -0
  13. ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
  14. ergon/bootstrap/src/__project__/tasks/example_task/config.py +4 -0
  15. ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +4 -0
  16. ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +4 -0
  17. ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +5 -0
  18. ergon/bootstrap/src/__project__/tasks/example_task/task.py +1 -0
  19. ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
  20. ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
  21. ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
  22. ergon/bootstrap/src/__project__/tasks/settings.py +5 -0
  23. ergon/cli.py +174 -0
  24. ergon/connector/__init__.py +64 -0
  25. ergon/connector/connector.py +97 -0
  26. ergon/connector/excel/__init__.py +18 -0
  27. ergon/connector/excel/connector.py +175 -0
  28. ergon/connector/excel/models.py +24 -0
  29. ergon/connector/excel/service.py +98 -0
  30. ergon/connector/pipefy/__init__.py +21 -0
  31. ergon/connector/pipefy/async_connector.py +48 -0
  32. ergon/connector/pipefy/async_service.py +907 -0
  33. ergon/connector/pipefy/connector.py +36 -0
  34. ergon/connector/pipefy/models.py +48 -0
  35. ergon/connector/pipefy/service.py +1016 -0
  36. ergon/connector/pipefy/version.py +1 -0
  37. ergon/connector/postgres/__init__.py +11 -0
  38. ergon/connector/postgres/async_connector.py +119 -0
  39. ergon/connector/postgres/async_service.py +116 -0
  40. ergon/connector/postgres/models.py +34 -0
  41. ergon/connector/rabbitmq/__init__.py +25 -0
  42. ergon/connector/rabbitmq/async_connector.py +120 -0
  43. ergon/connector/rabbitmq/async_service.py +417 -0
  44. ergon/connector/rabbitmq/connector.py +54 -0
  45. ergon/connector/rabbitmq/helper.py +14 -0
  46. ergon/connector/rabbitmq/models.py +92 -0
  47. ergon/connector/rabbitmq/service.py +199 -0
  48. ergon/connector/sqs/__init__.py +15 -0
  49. ergon/connector/sqs/async_connector.py +120 -0
  50. ergon/connector/sqs/async_service.py +246 -0
  51. ergon/connector/sqs/connector.py +120 -0
  52. ergon/connector/sqs/models.py +36 -0
  53. ergon/connector/sqs/service.py +219 -0
  54. ergon/connector/transaction.py +14 -0
  55. ergon/py.typed +0 -0
  56. ergon/service/__init__.py +5 -0
  57. ergon/service/service.py +17 -0
  58. ergon/task/__init__.py +13 -0
  59. ergon/task/base.py +222 -0
  60. ergon/task/exceptions.py +217 -0
  61. ergon/task/helpers.py +691 -0
  62. ergon/task/manager.py +85 -0
  63. ergon/task/mixins/__init__.py +13 -0
  64. ergon/task/mixins/consumer.py +858 -0
  65. ergon/task/mixins/metrics.py +457 -0
  66. ergon/task/mixins/producer.py +486 -0
  67. ergon/task/policies.py +229 -0
  68. ergon/task/runner.py +386 -0
  69. ergon/task/utils.py +64 -0
  70. ergon/telemetry/__init__.py +7 -0
  71. ergon/telemetry/_resource.py +13 -0
  72. ergon/telemetry/logging.py +370 -0
  73. ergon/telemetry/metrics.py +101 -0
  74. ergon/telemetry/tracing.py +152 -0
  75. ergon/utils/__init__.py +5 -0
  76. ergon/utils/env.py +26 -0
  77. ergon_framework_python-0.1.0.dist-info/METADATA +449 -0
  78. ergon_framework_python-0.1.0.dist-info/RECORD +82 -0
  79. ergon_framework_python-0.1.0.dist-info/WHEEL +5 -0
  80. ergon_framework_python-0.1.0.dist-info/entry_points.txt +2 -0
  81. ergon_framework_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  82. ergon_framework_python-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,486 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+ from abc import ABC, abstractmethod
5
+ from concurrent import futures
6
+ from typing import Any, List
7
+
8
+ from more_itertools import chunked
9
+
10
+ from ... import connector, telemetry
11
+ from .. import base, exceptions, helpers, policies
12
+ from . import metrics as mixin_metrics
13
+
14
+ logger = logging.getLogger(__name__)
15
+ tracer = telemetry.tracing.get_tracer(__name__)
16
+
17
+ # -------------------------------------------------------------------
18
+ # PRODUCER MIXIN (SYNC)
19
+ # -------------------------------------------------------------------
20
+
21
+
22
+ class ProducerMixin(ABC):
23
+ name: str
24
+
25
+ # -------------------------------------------------------------------
26
+ # HOOKS
27
+ # -------------------------------------------------------------------
28
+ @abstractmethod
29
+ def prepare_transaction(self, transaction: connector.Transaction) -> Any:
30
+ raise NotImplementedError
31
+
32
+ def handle_prepare_success(self, transaction: connector.Transaction, result: Any):
33
+ logger.debug(f"[{self.name}] SUCCESS → {transaction.id}")
34
+
35
+ def handle_prepare_exception(self, transaction: connector.Transaction, exc: exceptions.TransactionException):
36
+ logger.error(f"[{self.name}] EXCEPTION → {transaction.id}: {exc}")
37
+
38
+ # -------------------------------------------------------------------
39
+ # PRODUCE 1 ITEM (FULL RETRY LIFECYCLE)
40
+ # -------------------------------------------------------------------
41
+ def _start_producing(self, transaction: connector.Transaction, policy: policies.ProducerPolicy):
42
+ """
43
+ PRODUCE → SUCCESS | EXCEPTION
44
+ """
45
+ tx_start = time.perf_counter()
46
+ final_status = "success"
47
+
48
+ try:
49
+ # -----------------------
50
+ # 1) PREPARE
51
+ # -----------------------
52
+ prepare_success, prepare_result = self._handle_prepare(transaction, policy.prepare)
53
+
54
+ # -----------------------
55
+ # 2) EXCEPTION HANDLER
56
+ # -----------------------
57
+ if not prepare_success:
58
+ final_status = "exception"
59
+ if isinstance(prepare_result, exceptions.TransactionException):
60
+ prepare_result = prepare_result
61
+ elif isinstance(prepare_result, futures.TimeoutError):
62
+ prepare_result = exceptions.TransactionException(
63
+ str(prepare_result), exceptions.ExceptionType.TIMEOUT
64
+ )
65
+ else:
66
+ prepare_result = exceptions.TransactionException(
67
+ str(prepare_result), exceptions.ExceptionType.SYSTEM
68
+ )
69
+ return self._handle_prepare_exception(transaction, prepare_result, policy.exception)
70
+
71
+ # -----------------------
72
+ # 3) SUCCESS HANDLER
73
+ # -----------------------
74
+ success_success, success_result = self._handle_prepare_success(
75
+ transaction, prepare_result, policy.success, policy.exception
76
+ )
77
+ if not success_success:
78
+ final_status = "exception"
79
+ if isinstance(success_result, exceptions.TransactionException):
80
+ success_result = success_result
81
+ elif isinstance(success_result, futures.TimeoutError):
82
+ success_result = exceptions.TransactionException(
83
+ str(success_result), exceptions.ExceptionType.TIMEOUT
84
+ )
85
+ else:
86
+ success_result = exceptions.TransactionException(
87
+ str(success_result), exceptions.ExceptionType.SYSTEM
88
+ )
89
+ return self._handle_prepare_exception(transaction, success_result, policy.exception)
90
+
91
+ return True, success_result
92
+ finally:
93
+ # Record transaction-level metrics
94
+ tx_duration = time.perf_counter() - tx_start
95
+ mixin_metrics.record_producer_transaction(
96
+ task_name=getattr(self, "name", self.__class__.__name__),
97
+ transaction_id=transaction.id,
98
+ duration=tx_duration,
99
+ status=final_status,
100
+ )
101
+
102
+ # -------------------------------------------------------------------
103
+ # PREPARE HANDLER
104
+ # -------------------------------------------------------------------
105
+ def _handle_prepare(self, transaction: connector.Transaction, policy: policies.PreparePolicy):
106
+ logger.debug(f"[Producer] _handle_prepare called for transaction {transaction.id}")
107
+ stage_start = time.perf_counter()
108
+ success, result = helpers.run_fn(
109
+ fn=lambda: self.prepare_transaction(transaction),
110
+ retry=policy.retry,
111
+ trace_name=f"{self.__class__.__name__}.prepare",
112
+ trace_attrs={"transaction": transaction.id},
113
+ )
114
+ # Record lifecycle metrics
115
+ mixin_metrics.record_producer_lifecycle(
116
+ task_name=getattr(self, "name", self.__class__.__name__),
117
+ stage="prepare",
118
+ duration=time.perf_counter() - stage_start,
119
+ outcome="ok" if success else "error",
120
+ )
121
+ return success, result
122
+
123
+ # -------------------------------------------------------------------
124
+ # SUCCESS HANDLER
125
+ # -------------------------------------------------------------------
126
+ def _handle_prepare_success(
127
+ self,
128
+ transaction: connector.Transaction,
129
+ result: Any,
130
+ policy: policies.SuccessPolicy,
131
+ exception_policy: policies.ExceptionPolicy,
132
+ ):
133
+ stage_start = time.perf_counter()
134
+ success, handler_result = helpers.run_fn(
135
+ fn=lambda: self.handle_prepare_success(transaction, result),
136
+ retry=policy.retry,
137
+ trace_name=f"{self.__class__.__name__}.handle_prepare_success",
138
+ trace_attrs={"transaction": transaction.id},
139
+ )
140
+ # Record lifecycle metrics
141
+ mixin_metrics.record_producer_lifecycle(
142
+ task_name=getattr(self, "name", self.__class__.__name__),
143
+ stage="success",
144
+ duration=time.perf_counter() - stage_start,
145
+ outcome="ok" if success else "error",
146
+ )
147
+ return success, handler_result
148
+
149
+ # -------------------------------------------------------------------
150
+ # EXCEPTION HANDLER
151
+ # -------------------------------------------------------------------
152
+ def _handle_prepare_exception(
153
+ self,
154
+ transaction: connector.Transaction,
155
+ exc: exceptions.TransactionException,
156
+ policy: policies.ExceptionPolicy,
157
+ ):
158
+ stage_start = time.perf_counter()
159
+ success, result = helpers.run_fn(
160
+ fn=lambda: self.handle_prepare_exception(transaction, exc),
161
+ retry=policy.retry,
162
+ trace_name=f"{self.__class__.__name__}.handle_prepare_exception",
163
+ trace_attrs={"transaction": transaction.id},
164
+ )
165
+ # Record lifecycle metrics
166
+ mixin_metrics.record_producer_lifecycle(
167
+ task_name=getattr(self, "name", self.__class__.__name__),
168
+ stage="exception",
169
+ duration=time.perf_counter() - stage_start,
170
+ outcome="ok" if success else "error",
171
+ )
172
+ return success, result
173
+
174
+ # -------------------------------------------------------------------
175
+ # PUBLIC API — PRODUCE MANY
176
+ # -------------------------------------------------------------------
177
+ def produce_transactions(
178
+ self,
179
+ transactions: List[connector.Transaction],
180
+ policy: policies.ProducerPolicy | None = None,
181
+ ):
182
+ if policy is None:
183
+ policy = policies.ProducerPolicy()
184
+
185
+ def _produce():
186
+ start_time = time.perf_counter()
187
+ processed = 0
188
+ executor = futures.ThreadPoolExecutor(max_workers=policy.loop.concurrency.value)
189
+
190
+ # ============================================================
191
+ # SUBMIT FUNCTION
192
+ # ============================================================
193
+ def submit_start_producing(tr, pol):
194
+ return helpers.run_fn(
195
+ fn=lambda: self._start_producing(transaction=tr, policy=pol),
196
+ executor=executor,
197
+ trace_name=f"{self.__class__.__name__}.start_producing",
198
+ trace_attrs={"transaction_id": tr.id},
199
+ )
200
+
201
+ # ============================================================
202
+ # PRODUCE LOOP
203
+ # ============================================================
204
+ batches = list(chunked(transactions, policy.loop.batch.size))
205
+ for batch_number, batch in enumerate(batches, 1):
206
+ # Record batch metric
207
+ mixin_metrics.record_producer_batch(
208
+ task_name=getattr(self, "name", self.__class__.__name__),
209
+ batch_number=batch_number,
210
+ batch_size=len(batch),
211
+ )
212
+
213
+ def submissions():
214
+ for tr in batch:
215
+ yield lambda tr=tr: submit_start_producing(tr, policy)
216
+
217
+ count = helpers.multithread_execute(
218
+ submissions=submissions(),
219
+ concurrency=policy.loop.concurrency.value,
220
+ limit=policy.loop.limit,
221
+ timeout=policy.transaction_runtime.timeout,
222
+ )
223
+
224
+ processed += count
225
+
226
+ if policy.loop.limit and processed >= policy.loop.limit:
227
+ break
228
+
229
+ executor.shutdown()
230
+ elapsed_time = time.perf_counter() - start_time
231
+ logger.info(f"[Produce] Finished. Processed={processed} in {elapsed_time:.2f} seconds")
232
+ return processed
233
+
234
+ success, result = helpers.run_fn(
235
+ fn=lambda: _produce(),
236
+ retry=policies.RetryPolicy(timeout=policy.loop.timeout),
237
+ trace_name=f"{self.__class__.__name__}.produce_transactions",
238
+ trace_attrs={"count": len(transactions)},
239
+ )
240
+
241
+ if not success:
242
+ if isinstance(result, futures.TimeoutError):
243
+ raise exceptions.ProducerLoopTimeoutException(str(result))
244
+ raise result
245
+ return result
246
+
247
+
248
+ class ProducerTask(ProducerMixin, base.BaseTask):
249
+ """
250
+ Backwards-compatible producer task.
251
+ You can still inherit from this if you're only a producer.
252
+ """
253
+
254
+ pass
255
+
256
+
257
+ # -------------------------------------------------------------------
258
+ # ASYNC PRODUCER MIXIN
259
+ # -------------------------------------------------------------------
260
+
261
+
262
+ class AsyncProducerMixin(ABC):
263
+ name: str
264
+
265
+ # -------------------------------------------------------------------
266
+ # HOOKS
267
+ # -------------------------------------------------------------------
268
+ @abstractmethod
269
+ async def prepare_transaction(self, transaction: connector.Transaction) -> Any:
270
+ raise NotImplementedError
271
+
272
+ async def handle_prepare_success(self, transaction: connector.Transaction, result: Any):
273
+ logger.debug(f"[{self.name}] SUCCESS → {transaction.id}")
274
+
275
+ async def handle_prepare_exception(self, transaction: connector.Transaction, exc: exceptions.TransactionException):
276
+ logger.error(f"[{self.name}] EXCEPTION → {transaction.id}: {exc}")
277
+
278
+ # -------------------------------------------------------------------
279
+ # PRODUCE 1 ITEM (FULL RETRY LIFECYCLE)
280
+ # -------------------------------------------------------------------
281
+ async def _start_producing(self, transaction: connector.Transaction, policy: policies.ProducerPolicy):
282
+ """
283
+ PRODUCE → SUCCESS | EXCEPTION
284
+ """
285
+ tx_start = time.perf_counter()
286
+ final_status = "success"
287
+
288
+ try:
289
+ # -----------------------
290
+ # 1) PREPARE
291
+ # -----------------------
292
+ prepare_success, prepare_result = await self._handle_prepare(transaction, policy.prepare)
293
+
294
+ # -----------------------
295
+ # 2) EXCEPTION HANDLER
296
+ # -----------------------
297
+ if not prepare_success:
298
+ final_status = "exception"
299
+ if isinstance(prepare_result, exceptions.TransactionException):
300
+ prepare_result = prepare_result
301
+ elif isinstance(prepare_result, (asyncio.TimeoutError, futures.TimeoutError)):
302
+ prepare_result = exceptions.TransactionException(
303
+ str(prepare_result), exceptions.ExceptionType.TIMEOUT
304
+ )
305
+ else:
306
+ prepare_result = exceptions.TransactionException(
307
+ str(prepare_result), exceptions.ExceptionType.SYSTEM
308
+ )
309
+ return await self._handle_prepare_exception(transaction, prepare_result, policy.exception)
310
+
311
+ # -----------------------
312
+ # 3) SUCCESS HANDLER
313
+ # -----------------------
314
+ success_success, success_result = await self._handle_prepare_success(
315
+ transaction, prepare_result, policy.success, policy.exception
316
+ )
317
+ if not success_success:
318
+ final_status = "exception"
319
+ if isinstance(success_result, exceptions.TransactionException):
320
+ success_result = success_result
321
+ elif isinstance(success_result, (asyncio.TimeoutError, futures.TimeoutError)):
322
+ success_result = exceptions.TransactionException(
323
+ str(success_result), exceptions.ExceptionType.TIMEOUT
324
+ )
325
+ else:
326
+ success_result = exceptions.TransactionException(
327
+ str(success_result), exceptions.ExceptionType.SYSTEM
328
+ )
329
+ return await self._handle_prepare_exception(transaction, success_result, policy.exception)
330
+
331
+ return True, success_result
332
+ finally:
333
+ tx_duration = time.perf_counter() - tx_start
334
+ mixin_metrics.record_producer_transaction(
335
+ task_name=getattr(self, "name", self.__class__.__name__),
336
+ transaction_id=transaction.id,
337
+ duration=tx_duration,
338
+ status=final_status,
339
+ )
340
+
341
+ # -------------------------------------------------------------------
342
+ # PREPARE HANDLER
343
+ # -------------------------------------------------------------------
344
+ async def _handle_prepare(self, transaction: connector.Transaction, policy: policies.PreparePolicy):
345
+ logger.debug(f"[Producer] _handle_prepare called for transaction {transaction.id}")
346
+ stage_start = time.perf_counter()
347
+ success, result = await helpers.run_fn_async(
348
+ fn=lambda: self.prepare_transaction(transaction),
349
+ retry=policy.retry,
350
+ trace_name=f"{self.__class__.__name__}.prepare",
351
+ trace_attrs={"transaction": transaction.id},
352
+ )
353
+ mixin_metrics.record_producer_lifecycle(
354
+ task_name=getattr(self, "name", self.__class__.__name__),
355
+ stage="prepare",
356
+ duration=time.perf_counter() - stage_start,
357
+ outcome="ok" if success else "error",
358
+ )
359
+ return success, result
360
+
361
+ # -------------------------------------------------------------------
362
+ # SUCCESS HANDLER
363
+ # -------------------------------------------------------------------
364
+ async def _handle_prepare_success(
365
+ self,
366
+ transaction: connector.Transaction,
367
+ result: Any,
368
+ policy: policies.SuccessPolicy,
369
+ exception_policy: policies.ExceptionPolicy,
370
+ ):
371
+ stage_start = time.perf_counter()
372
+ success, handler_result = await helpers.run_fn_async(
373
+ fn=lambda: self.handle_prepare_success(transaction, result),
374
+ retry=policy.retry,
375
+ trace_name=f"{self.__class__.__name__}.handle_prepare_success",
376
+ trace_attrs={"transaction": transaction.id},
377
+ )
378
+ mixin_metrics.record_producer_lifecycle(
379
+ task_name=getattr(self, "name", self.__class__.__name__),
380
+ stage="success",
381
+ duration=time.perf_counter() - stage_start,
382
+ outcome="ok" if success else "error",
383
+ )
384
+ return success, handler_result
385
+
386
+ # -------------------------------------------------------------------
387
+ # EXCEPTION HANDLER
388
+ # -------------------------------------------------------------------
389
+ async def _handle_prepare_exception(
390
+ self,
391
+ transaction: connector.Transaction,
392
+ exc: exceptions.TransactionException,
393
+ policy: policies.ExceptionPolicy,
394
+ ):
395
+ stage_start = time.perf_counter()
396
+ success, result = await helpers.run_fn_async(
397
+ fn=lambda: self.handle_prepare_exception(transaction, exc),
398
+ retry=policy.retry,
399
+ trace_name=f"{self.__class__.__name__}.handle_prepare_exception",
400
+ trace_attrs={"transaction": transaction.id},
401
+ )
402
+ mixin_metrics.record_producer_lifecycle(
403
+ task_name=getattr(self, "name", self.__class__.__name__),
404
+ stage="exception",
405
+ duration=time.perf_counter() - stage_start,
406
+ outcome="ok" if success else "error",
407
+ )
408
+ return success, result
409
+
410
+ # -------------------------------------------------------------------
411
+ # PUBLIC API — PRODUCE MANY
412
+ # -------------------------------------------------------------------
413
+ async def produce_transactions(
414
+ self,
415
+ transactions: List[connector.Transaction],
416
+ policy: policies.ProducerPolicy | None = None,
417
+ ):
418
+ if policy is None:
419
+ policy = policies.ProducerPolicy()
420
+
421
+ async def _produce():
422
+ start_time = time.perf_counter()
423
+ processed = 0
424
+
425
+ # ============================================================
426
+ # SUBMIT FUNCTION
427
+ # ============================================================
428
+ async def submit_start_producing(tr, pol):
429
+ return await helpers.run_fn_async(
430
+ fn=lambda: self._start_producing(transaction=tr, policy=pol),
431
+ trace_name=f"{self.__class__.__name__}.start_producing",
432
+ trace_attrs={"transaction_id": tr.id},
433
+ )
434
+
435
+ # ============================================================
436
+ # PRODUCE LOOP
437
+ # ============================================================
438
+ batches = list(chunked(transactions, policy.loop.batch.size))
439
+ for batch_number, batch in enumerate(batches, 1):
440
+ mixin_metrics.record_producer_batch(
441
+ task_name=getattr(self, "name", self.__class__.__name__),
442
+ batch_number=batch_number,
443
+ batch_size=len(batch),
444
+ )
445
+
446
+ def submissions():
447
+ for tr in batch:
448
+ yield lambda tr=tr: asyncio.create_task(submit_start_producing(tr, policy))
449
+
450
+ count = await helpers.async_execute(
451
+ submissions=submissions(),
452
+ concurrency=policy.loop.concurrency.value,
453
+ limit=policy.loop.limit,
454
+ timeout=policy.transaction_runtime.timeout,
455
+ )
456
+
457
+ processed += count
458
+
459
+ if policy.loop.limit and processed >= policy.loop.limit:
460
+ break
461
+
462
+ elapsed_time = time.perf_counter() - start_time
463
+ logger.info(f"[Produce] Finished. Processed={processed} in {elapsed_time:.2f} seconds")
464
+ return processed
465
+
466
+ success, result = await helpers.run_fn_async(
467
+ fn=_produce,
468
+ retry=policies.RetryPolicy(timeout=policy.loop.timeout),
469
+ trace_name=f"{self.__class__.__name__}.produce_transactions",
470
+ trace_attrs={"count": len(transactions)},
471
+ )
472
+
473
+ if not success:
474
+ if isinstance(result, asyncio.TimeoutError):
475
+ raise exceptions.ProducerLoopTimeoutException(str(result))
476
+ raise result
477
+ return result
478
+
479
+
480
+ class AsyncProducerTask(AsyncProducerMixin, base.BaseAsyncTask):
481
+ """
482
+ Async producer task.
483
+ You can still inherit from this if you're only an async producer.
484
+ """
485
+
486
+ pass