ergon-framework-python 0.1.8__tar.gz → 0.1.9__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 (105) hide show
  1. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/PKG-INFO +1 -1
  2. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/pyproject.toml +2 -2
  3. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/__init__.py +1 -1
  4. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/consumer.py +146 -7
  5. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/policies.py +10 -1
  6. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/PKG-INFO +1 -1
  7. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/tests/test_smoke.py +1 -1
  8. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/LICENSE +0 -0
  9. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/README.md +0 -0
  10. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/setup.cfg +0 -0
  11. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/__init__.py +0 -0
  12. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +0 -0
  13. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/grafana.yaml +0 -0
  14. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/loki.yaml +0 -0
  15. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +0 -0
  16. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/prometheus.yaml +0 -0
  17. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/_observability/tempo.yaml +0 -0
  18. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
  19. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/main.py +0 -0
  20. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
  21. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/constants.py +0 -0
  22. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
  23. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/config.py +0 -0
  24. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +0 -0
  25. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +0 -0
  26. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +0 -0
  27. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/example_task/task.py +0 -0
  28. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
  29. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
  30. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
  31. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/bootstrap/src/__project__/tasks/settings.py +0 -0
  32. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/cli.py +0 -0
  33. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/__init__.py +0 -0
  34. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/connector.py +0 -0
  35. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/__init__.py +0 -0
  36. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/_client.py +0 -0
  37. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/_operations.py +0 -0
  38. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/async_connector.py +0 -0
  39. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/connector.py +0 -0
  40. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/models.py +0 -0
  41. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/ergon_platform/utils.py +0 -0
  42. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/__init__.py +0 -0
  43. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/connector.py +0 -0
  44. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/models.py +0 -0
  45. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/excel/service.py +0 -0
  46. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/__init__.py +0 -0
  47. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/async_auth_service.py +0 -0
  48. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/async_connector.py +0 -0
  49. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/async_service.py +0 -0
  50. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/auth_service.py +0 -0
  51. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/connector.py +0 -0
  52. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/models.py +0 -0
  53. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/service.py +0 -0
  54. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/nylas/utils.py +0 -0
  55. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/__init__.py +0 -0
  56. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/async_connector.py +0 -0
  57. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/async_service.py +0 -0
  58. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/connector.py +0 -0
  59. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/models.py +0 -0
  60. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/service.py +0 -0
  61. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/pipefy/version.py +0 -0
  62. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/__init__.py +0 -0
  63. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/async_connector.py +0 -0
  64. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/async_service.py +0 -0
  65. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/postgres/models.py +0 -0
  66. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/__init__.py +0 -0
  67. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/async_connector.py +0 -0
  68. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/async_service.py +0 -0
  69. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/connector.py +0 -0
  70. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/helper.py +0 -0
  71. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/models.py +0 -0
  72. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/rabbitmq/service.py +0 -0
  73. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/__init__.py +0 -0
  74. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/async_connector.py +0 -0
  75. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/async_service.py +0 -0
  76. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/connector.py +0 -0
  77. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/models.py +0 -0
  78. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/sqs/service.py +0 -0
  79. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/connector/transaction.py +0 -0
  80. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/py.typed +0 -0
  81. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/service/__init__.py +0 -0
  82. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/service/service.py +0 -0
  83. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/__init__.py +0 -0
  84. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/base.py +0 -0
  85. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/exceptions.py +0 -0
  86. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/helpers.py +0 -0
  87. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/liveness.py +0 -0
  88. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/manager.py +0 -0
  89. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/__init__.py +0 -0
  90. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/metrics.py +0 -0
  91. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/mixins/producer.py +0 -0
  92. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/runner.py +0 -0
  93. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/task/utils.py +0 -0
  94. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/telemetry/__init__.py +0 -0
  95. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/telemetry/_resource.py +0 -0
  96. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/telemetry/logging.py +0 -0
  97. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/telemetry/metrics.py +0 -0
  98. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/telemetry/tracing.py +0 -0
  99. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/utils/__init__.py +0 -0
  100. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon/utils/env.py +0 -0
  101. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/SOURCES.txt +0 -0
  102. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/dependency_links.txt +0 -0
  103. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/entry_points.txt +0 -0
  104. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/requires.txt +0 -0
  105. {ergon_framework_python-0.1.8 → ergon_framework_python-0.1.9}/src/ergon_framework_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ergon-framework-python
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Ergon internal task-oriented project bootstrapper
5
5
  Author-email: Ergondata Technologies <anza.vossos@protonmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ergon-framework-python"
3
- version = "0.1.8"
3
+ version = "0.1.9"
4
4
  description = "Ergon internal task-oriented project bootstrapper"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -47,7 +47,7 @@ dev = [
47
47
  "pyright>=1.1.390",
48
48
  "pytest>=8.0.0",
49
49
  "pytest-asyncio>=0.25.0",
50
- "ruff>=0.6.0",
50
+ "ruff>=0.6.0,<0.16",
51
51
  ]
52
52
 
53
53
  [build-system]
@@ -10,4 +10,4 @@ __all__ = [
10
10
  "manager",
11
11
  ]
12
12
 
13
- __version__ = "0.1.8"
13
+ __version__ = "0.1.9"
@@ -601,22 +601,28 @@ class AsyncConsumerMixin(ABC):
601
601
  # =====================================================================
602
602
  # FETCH HANDLER (ASYNC)
603
603
  # =====================================================================
604
- async def _handle_fetch(self, conn, policy: policies.FetchPolicy) -> tuple[bool, List[connector.Transaction]]:
604
+ async def _handle_fetch(
605
+ self,
606
+ conn,
607
+ policy: policies.FetchPolicy,
608
+ batch_size: int | None = None,
609
+ ) -> tuple[bool, List[connector.Transaction]]:
610
+ fetch_size = policy.batch.size if batch_size is None else batch_size
605
611
  self._mark_consumer_progress("fetching")
606
- logger.info(f"Fetch handler started for batch size {policy.batch.size}", extra=policy.extra)
612
+ logger.info(f"Fetch handler started for batch size {fetch_size}", extra=policy.extra)
607
613
  fetch_start = time.perf_counter()
608
614
  success, result = await helpers.run_fn_async(
609
- fn=lambda: conn.fetch_transactions_async(policy.batch.size, **policy.extra),
615
+ fn=lambda: conn.fetch_transactions_async(fetch_size, **policy.extra),
610
616
  retry=policy.retry,
611
617
  trace_name=f"{self.__class__.__name__}.fetch_transactions",
612
- trace_attrs={"batch_size": policy.batch.size},
618
+ trace_attrs={"batch_size": fetch_size},
613
619
  )
614
620
  # Record fetch metrics
615
621
  fetched_count = len(result) if success and result else 0
616
622
  mixin_metrics.record_consumer_fetch(
617
623
  task_name=getattr(self, "name", self.__class__.__name__),
618
624
  connector_name=conn.__class__.__name__,
619
- batch_size=policy.batch.size,
625
+ batch_size=fetch_size,
620
626
  fetched_count=fetched_count,
621
627
  duration=time.perf_counter() - fetch_start,
622
628
  success=success,
@@ -877,13 +883,146 @@ class AsyncConsumerMixin(ABC):
877
883
  )
878
884
  return await self._handle_exception(tr, timeout_error, pol.exception.retry)
879
885
 
886
+ async def _consume_continuous():
887
+ nonlocal batch_number, empty_count, processed
888
+
889
+ concurrency = policy.loop.concurrency.value
890
+ submitted = 0
891
+ in_flight: set[asyncio.Task] = set()
892
+
893
+ async def reap(done: set[asyncio.Task]) -> None:
894
+ nonlocal processed
895
+ for task in done:
896
+ try:
897
+ await task
898
+ except Exception as exc:
899
+ logger.error("[async] Execution error: %s", exc)
900
+ finally:
901
+ processed += 1
902
+
903
+ async def wait_for_completion() -> None:
904
+ done, _ = await asyncio.wait(
905
+ in_flight,
906
+ return_when=asyncio.FIRST_COMPLETED,
907
+ )
908
+ in_flight.difference_update(done)
909
+ await reap(done)
910
+ self._mark_consumer_progress("polling")
911
+
912
+ try:
913
+ while True:
914
+ if policy.loop.limit is not None and submitted >= policy.loop.limit:
915
+ if in_flight:
916
+ await wait_for_completion()
917
+ continue
918
+ break
919
+
920
+ free_slots = concurrency - len(in_flight)
921
+ if free_slots == 0:
922
+ await wait_for_completion()
923
+ continue
924
+
925
+ if policy.loop.limit is not None:
926
+ free_slots = min(free_slots, policy.loop.limit - submitted)
927
+
928
+ logger.debug(
929
+ "Fetching transactions for %d free continuous-consumer slot(s) with fetch policy: %s",
930
+ free_slots,
931
+ policy.fetch.model_dump_json(indent=2),
932
+ )
933
+ success, result = await self._handle_fetch(
934
+ conn,
935
+ policy.fetch,
936
+ batch_size=free_slots,
937
+ )
938
+
939
+ if not success:
940
+ logger.error("Fetch failed → %s", result)
941
+ # Unlike shutdown/cancellation, a fetch failure must not
942
+ # cancel work already in progress: let in-flight
943
+ # transactions finish (bounded by the transaction
944
+ # runtime timeout) before propagating the failure.
945
+ if in_flight:
946
+ logger.warning(
947
+ "Fetch failed with %d transaction(s) in flight; draining before raising",
948
+ len(in_flight),
949
+ )
950
+ await asyncio.gather(*in_flight, return_exceptions=True)
951
+ in_flight.clear()
952
+ if isinstance(result, (asyncio.TimeoutError, futures.TimeoutError)):
953
+ raise exceptions.FetchTimeoutException(str(result))
954
+ raise exceptions.FetchException(str(result))
955
+
956
+ transactions = result
957
+ if transactions:
958
+ empty_count = 0
959
+ batch_number += 1
960
+ self._mark_consumer_progress("processing")
961
+ logger.info(
962
+ "%d transaction(s) fetched for continuous processing",
963
+ len(transactions),
964
+ )
965
+ mixin_metrics.record_consumer_batch(
966
+ task_name=getattr(self, "name", self.__class__.__name__),
967
+ batch_number=batch_number,
968
+ batch_size=len(transactions),
969
+ streaming=policy.loop.streaming,
970
+ )
971
+ for transaction in transactions:
972
+ in_flight.add(
973
+ asyncio.create_task(
974
+ submit_start_processing(transaction, policy),
975
+ )
976
+ )
977
+ submitted += 1
978
+ continue
979
+
980
+ if in_flight:
981
+ await wait_for_completion()
982
+ continue
983
+
984
+ self._mark_consumer_progress("idle")
985
+ logger.info("Empty fetch detected at %s", datetime.now().isoformat())
986
+ if not policy.loop.streaming:
987
+ logger.info("Non-streaming mode detected, breaking loop")
988
+ break
989
+
990
+ mixin_metrics.record_consumer_empty_queue_wait(
991
+ task_name=getattr(self, "name", self.__class__.__name__),
992
+ wait_count=empty_count,
993
+ )
994
+ await utils.backoff_async(
995
+ backoff=policy.fetch.empty.backoff,
996
+ multiplier=policy.fetch.empty.backoff_multiplier,
997
+ cap=policy.fetch.empty.backoff_cap,
998
+ attempt=empty_count,
999
+ )
1000
+ empty_count += 1
1001
+ except BaseException:
1002
+ for task in in_flight:
1003
+ task.cancel()
1004
+ if in_flight:
1005
+ await asyncio.gather(*in_flight, return_exceptions=True)
1006
+ raise
1007
+
1008
+ elapsed_time = time.perf_counter() - start_time
1009
+ logger.info(
1010
+ "[Consume continuous] Finished. Processed=%d in %.2f seconds",
1011
+ processed,
1012
+ elapsed_time,
1013
+ )
1014
+ return processed
1015
+
1016
+ if policy.loop.mode == "continuous":
1017
+ return await _consume_continuous()
1018
+
880
1019
  while True:
881
1020
  batch_number += 1
882
1021
 
883
1022
  # ============================================================
884
1023
  # FETCH
885
1024
  # ============================================================
886
- logger.info(f"Fetching transactions batch with fetch policy: {policy.fetch.model_dump_json(indent=2)}")
1025
+ logger.debug(f"Fetching transactions batch with fetch policy: {policy.fetch.model_dump_json(indent=2)}")
887
1026
  success, result = await self._handle_fetch(conn, policy.fetch)
888
1027
 
889
1028
  if not success:
@@ -941,7 +1080,7 @@ class AsyncConsumerMixin(ABC):
941
1080
  else:
942
1081
  batch_context = None # Use current context
943
1082
 
944
- logger.info(
1083
+ logger.debug(
945
1084
  f"Starting batch processing of "
946
1085
  f"{len(transactions)} transaction(s) "
947
1086
  f"from fetch handler with "
@@ -1,4 +1,4 @@
1
- from typing import Optional, Union
1
+ from typing import Literal, Optional, Union
2
2
 
3
3
  from pydantic import BaseModel, Field, field_validator
4
4
 
@@ -161,11 +161,20 @@ class ExceptionPolicy(BaseModel):
161
161
 
162
162
 
163
163
  class ConsumerLoopPolicy(BaseModel):
164
+ mode: Literal["batch", "continuous"] = Field(default="batch")
164
165
  concurrency: ConcurrencyPolicy = Field(default_factory=ConcurrencyPolicy)
165
166
  timeout: Optional[float] = Field(default=None, ge=0)
166
167
  limit: Optional[int] = Field(default=None, ge=0)
167
168
  streaming: bool = Field(default=False)
168
169
 
170
+ @field_validator("mode", mode="before")
171
+ @classmethod
172
+ def _normalize_mode(cls, v):
173
+ v = _normalize_optional(v)
174
+ if v is None:
175
+ return "batch"
176
+ return v.strip().lower() if isinstance(v, str) else v
177
+
169
178
  @field_validator("timeout", "limit", mode="before")
170
179
  @classmethod
171
180
  def _normalize_optional_numbers(cls, v):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ergon-framework-python
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Ergon internal task-oriented project bootstrapper
5
5
  Author-email: Ergondata Technologies <anza.vossos@protonmail.com>
6
6
  License-Expression: MIT
@@ -4,4 +4,4 @@ import ergon
4
4
 
5
5
 
6
6
  def test_ergon_package_is_importable() -> None:
7
- assert ergon.__version__ == "0.1.8"
7
+ assert ergon.__version__ == "0.1.9"