edda-framework 0.12.0__py3-none-any.whl → 0.14.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.
edda/channels.py CHANGED
@@ -129,6 +129,24 @@ class WaitForTimerException(Exception):
129
129
  super().__init__(f"Waiting for timer: {timer_id}")
130
130
 
131
131
 
132
+ class ChannelModeConflictError(Exception):
133
+ """
134
+ Raised when subscribing with a different mode than the channel's established mode.
135
+
136
+ A channel's mode is locked when the first subscription is created. Subsequent
137
+ subscriptions must use the same mode.
138
+ """
139
+
140
+ def __init__(self, channel: str, existing_mode: str, requested_mode: str) -> None:
141
+ self.channel = channel
142
+ self.existing_mode = existing_mode
143
+ self.requested_mode = requested_mode
144
+ super().__init__(
145
+ f"Channel '{channel}' is already configured as '{existing_mode}' mode. "
146
+ f"Cannot subscribe with '{requested_mode}' mode."
147
+ )
148
+
149
+
132
150
  # =============================================================================
133
151
  # Subscription Functions
134
152
  # =============================================================================
@@ -150,6 +168,10 @@ async def subscribe(
150
168
  - "competing": Each message goes to only one subscriber (work queue pattern)
151
169
  - "direct": Receive messages sent via send_to() to this instance
152
170
 
171
+ Raises:
172
+ ChannelModeConflictError: If the channel is already configured with a different mode
173
+ ValueError: If mode is not 'broadcast', 'competing', or 'direct'
174
+
153
175
  The "direct" mode is syntactic sugar that subscribes to "channel:instance_id" internally,
154
176
  allowing simpler code when receiving direct messages:
155
177
 
@@ -204,6 +226,11 @@ async def subscribe(
204
226
  f"Invalid subscription mode: {mode}. Must be 'broadcast', 'competing', or 'direct'"
205
227
  )
206
228
 
229
+ # Check for mode conflict
230
+ existing_mode = await ctx.storage.get_channel_mode(actual_channel)
231
+ if existing_mode is not None and existing_mode != actual_mode:
232
+ raise ChannelModeConflictError(channel, existing_mode, mode)
233
+
207
234
  await ctx.storage.subscribe_to_channel(ctx.instance_id, actual_channel, actual_mode)
208
235
 
209
236
 
edda/replay.py CHANGED
@@ -368,16 +368,22 @@ class ReplayEngine:
368
368
  traceback.format_exception(type(error), error, error.__traceback__)
369
369
  )
370
370
 
371
- # Mark as failed with detailed error information
372
- await ctx._update_status(
373
- "failed",
374
- {
375
- "error_message": str(error),
376
- "error_type": type(error).__name__,
377
- "stack_trace": stack_trace,
378
- },
371
+ error_data = {
372
+ "error_message": str(error),
373
+ "error_type": type(error).__name__,
374
+ "stack_trace": stack_trace,
375
+ }
376
+
377
+ await self.storage.append_history(
378
+ instance_id=instance_id,
379
+ activity_id="workflow_failed",
380
+ event_type="WorkflowFailed",
381
+ event_data=error_data,
379
382
  )
380
383
 
384
+ # Mark as failed with detailed error information
385
+ await ctx._update_status("failed", error_data)
386
+
381
387
  # Call hook: workflow failed
382
388
  if self.hooks and hasattr(self.hooks, "on_workflow_failed"):
383
389
  await self.hooks.on_workflow_failed(instance_id, workflow_name, error)
@@ -576,15 +582,21 @@ class ReplayEngine:
576
582
  traceback.format_exception(type(error), error, error.__traceback__)
577
583
  )
578
584
 
579
- # Mark as failed with detailed error information
580
- await ctx._update_status(
581
- "failed",
582
- {
583
- "error_message": str(error),
584
- "error_type": type(error).__name__,
585
- "stack_trace": stack_trace,
586
- },
585
+ error_data = {
586
+ "error_message": str(error),
587
+ "error_type": type(error).__name__,
588
+ "stack_trace": stack_trace,
589
+ }
590
+
591
+ await self.storage.append_history(
592
+ instance_id=instance_id,
593
+ activity_id="workflow_failed",
594
+ event_type="WorkflowFailed",
595
+ event_data=error_data,
587
596
  )
597
+
598
+ # Mark as failed with detailed error information
599
+ await ctx._update_status("failed", error_data)
588
600
  raise
589
601
 
590
602
  async def resume_by_name(
@@ -775,15 +787,21 @@ class ReplayEngine:
775
787
  traceback.format_exception(type(error), error, error.__traceback__)
776
788
  )
777
789
 
778
- await ctx._update_status(
779
- "failed",
780
- {
781
- "error_message": str(error),
782
- "error_type": type(error).__name__,
783
- "stack_trace": stack_trace,
784
- },
790
+ error_data = {
791
+ "error_message": str(error),
792
+ "error_type": type(error).__name__,
793
+ "stack_trace": stack_trace,
794
+ }
795
+
796
+ await self.storage.append_history(
797
+ instance_id=new_instance_id,
798
+ activity_id="workflow_failed",
799
+ event_type="WorkflowFailed",
800
+ event_data=error_data,
785
801
  )
786
802
 
803
+ await ctx._update_status("failed", error_data)
804
+
787
805
  if self.hooks and hasattr(self.hooks, "on_workflow_failed"):
788
806
  await self.hooks.on_workflow_failed(new_instance_id, workflow_name, error)
789
807
 
edda/storage/protocol.py CHANGED
@@ -990,6 +990,18 @@ class StorageProtocol(Protocol):
990
990
  """
991
991
  ...
992
992
 
993
+ async def get_channel_mode(self, channel: str) -> str | None:
994
+ """
995
+ Get the mode for a channel (from any existing subscription).
996
+
997
+ Args:
998
+ channel: Channel name
999
+
1000
+ Returns:
1001
+ The mode ('broadcast' or 'competing') or None if no subscriptions exist
1002
+ """
1003
+ ...
1004
+
993
1005
  async def register_channel_receive_and_release_lock(
994
1006
  self,
995
1007
  instance_id: str,
@@ -3170,6 +3170,26 @@ class SQLAlchemyStorage:
3170
3170
  "cursor_message_id": subscription.cursor_message_id,
3171
3171
  }
3172
3172
 
3173
+ async def get_channel_mode(self, channel: str) -> str | None:
3174
+ """
3175
+ Get the mode for a channel (from any existing subscription).
3176
+
3177
+ Args:
3178
+ channel: Channel name
3179
+
3180
+ Returns:
3181
+ The mode ('broadcast' or 'competing') or None if no subscriptions exist
3182
+ """
3183
+ session = self._get_session_for_operation()
3184
+ async with self._session_scope(session) as session:
3185
+ result = await session.execute(
3186
+ select(ChannelSubscription.mode)
3187
+ .where(ChannelSubscription.channel == channel)
3188
+ .limit(1)
3189
+ )
3190
+ row = result.scalar_one_or_none()
3191
+ return row
3192
+
3173
3193
  async def register_channel_receive_and_release_lock(
3174
3194
  self,
3175
3195
  instance_id: str,
edda/viewer_ui/app.py CHANGED
@@ -309,6 +309,10 @@ def start_viewer(edda_app: EddaApp, port: int = 8080, reload: bool = False) -> N
309
309
  all_workflows = service.get_all_workflows()
310
310
  workflow_names = list(all_workflows.keys())
311
311
 
312
+ workflow_select: Any = None
313
+ params_container: Any = None
314
+ param_fields: dict[str, Any] = {}
315
+
312
316
  if not workflow_names:
313
317
  ui.label("No workflows registered").classes("text-red-500")
314
318
  ui.button("Close", on_click=start_dialog.close)
@@ -323,9 +327,6 @@ def start_viewer(edda_app: EddaApp, port: int = 8080, reload: bool = False) -> N
323
327
  # Container for dynamic parameter fields
324
328
  params_container = ui.column().classes("w-full mb-4")
325
329
 
326
- # Store input field references
327
- param_fields: dict[str, Any] = {}
328
-
329
330
  # Factory functions for creating field managers with proper closures
330
331
  # These must be defined outside the loop to avoid closure issues
331
332
 
@@ -778,6 +779,8 @@ def start_viewer(edda_app: EddaApp, port: int = 8080, reload: bool = False) -> N
778
779
 
779
780
  def update_parameter_fields() -> None:
780
781
  """Update parameter input fields based on selected workflow."""
782
+ if workflow_select is None or params_container is None:
783
+ return
781
784
  selected_workflow = workflow_select.value
782
785
  if not selected_workflow:
783
786
  return
@@ -1106,13 +1109,17 @@ def start_viewer(edda_app: EddaApp, port: int = 8080, reload: bool = False) -> N
1106
1109
  update_parameter_fields()
1107
1110
 
1108
1111
  # Update fields when workflow selection changes
1109
- workflow_select.on_value_change(lambda _: update_parameter_fields())
1112
+ if workflow_select is not None:
1113
+ workflow_select.on_value_change(lambda _: update_parameter_fields())
1110
1114
 
1111
1115
  # Action buttons
1112
1116
  with ui.row().classes("w-full gap-2"):
1113
1117
 
1114
1118
  async def handle_start() -> None:
1115
1119
  """Handle workflow start."""
1120
+ if workflow_select is None:
1121
+ ui.notify("No workflows available", type="negative")
1122
+ return
1116
1123
  try:
1117
1124
  selected_workflow = workflow_select.value
1118
1125
  if not selected_workflow:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edda-framework
3
- Version: 0.12.0
3
+ Version: 0.14.0
4
4
  Summary: Lightweight Durable Execution Framework
5
5
  Project-URL: Homepage, https://github.com/i2y/edda
6
6
  Project-URL: Documentation, https://github.com/i2y/edda#readme
@@ -1,14 +1,14 @@
1
1
  edda/__init__.py,sha256=hGC6WR2R36M8LWC97F-0Rw4Ln0QUUT_1xC-7acOy_Fk,2237
2
2
  edda/activity.py,sha256=nRm9eBrr0lFe4ZRQ2whyZ6mo5xd171ITIVhqytUhOpw,21025
3
3
  edda/app.py,sha256=ITTc7x5S4ykCP3KPZXKxuNczXkPtbn04ZQaxcem46Hw,68406
4
- edda/channels.py,sha256=CosFoB9HVHBKRmhU_t6qoCV3l6egAGt3sqpakfgZLKc,36596
4
+ edda/channels.py,sha256=6JFZkeOs0xDumexr0_bLI_Mb4S245hLJM_Sqp3xPCCA,37676
5
5
  edda/compensation.py,sha256=iKLlnTxiF1YSatmYQW84EkPB1yMKUEZBtgjuGnghLtY,11824
6
6
  edda/context.py,sha256=Qqm_nUC5NNnOfHAb7taqKqZVIc0GoRWUrjZ4L9_-q70,22128
7
7
  edda/exceptions.py,sha256=-ntBLGpVQgPFG5N1o8m_7weejAYkNrUdxTkOP38vsHk,1766
8
8
  edda/hooks.py,sha256=HUZ6FTM__DZjwuomDfTDEroQ3mugEPuJHcGm7CTQNvg,8193
9
9
  edda/locking.py,sha256=NAFJmw-JaSVsXn4Y4czJyv_s9bWG8cdrzDBWIEag5X8,13661
10
10
  edda/pydantic_utils.py,sha256=dGVPNrrttDeq1k233PopCtjORYjZitsgASPfPnO6R10,9056
11
- edda/replay.py,sha256=_poGUfvsDJP8GiflAw6aCZzxMKJpo99z__JVdGHb75I,42567
11
+ edda/replay.py,sha256=IQGByw9mlTpRulyUgsHJSPsZUULmM2YqFcm2WeB4jtw,43227
12
12
  edda/retry.py,sha256=t4_E1skrhotA1XWHTLbKi-DOgCMasOUnhI9OT-O_eCE,6843
13
13
  edda/workflow.py,sha256=hfBZM0JrtK0IkvZSrva0VmYVyvKCdiJ5FWFmIVENfrM,8807
14
14
  edda/wsgi.py,sha256=1pGE5fhHpcsYnDR8S3NEFKWUs5P0JK4roTAzX9BsIj0,2391
@@ -34,10 +34,10 @@ edda/storage/migrations.py,sha256=KrceouVODct9WWDBhmjAW0IYptDWd2mqJmhrHnee59M,13
34
34
  edda/storage/models.py,sha256=axXGJ-Orwcd_AsEUwIyFfDyg3NQxMcOQ2mrTzXkNv3g,12284
35
35
  edda/storage/notify_base.py,sha256=gUb-ypG1Bo0c-KrleYmC7eKtdwQNUeqGS5k7UILlSsQ,5055
36
36
  edda/storage/pg_notify.py,sha256=myzJ9xX86uiro9aaiA1SW1sN3E-zYafn7_lpeAy1jOg,11830
37
- edda/storage/protocol.py,sha256=vdB5GvBen8lgUA0qEfBXfQTLbVfGKeBTQuEwSUqLZtI,39463
38
- edda/storage/sqlalchemy_storage.py,sha256=HREK7fHmq3DGx6w4jA03_NrQu9HbyMomyIawMuOQLYQ,146246
37
+ edda/storage/protocol.py,sha256=tLUbD7SQ71oJVaTKfeh5HG1hvuLfaxoqC-a8m-iF0LY,39786
38
+ edda/storage/sqlalchemy_storage.py,sha256=UQmq3C_iC2j3N7q2V0kPcbtdwnFHAtyYWd9NBHVWsWQ,146934
39
39
  edda/viewer_ui/__init__.py,sha256=N1-T33SXadOXcBsDSgJJ9Iqz4y4verJngWryQu70c5c,517
40
- edda/viewer_ui/app.py,sha256=K3c5sMeJz_AE9gh5QftxwvfDthLeJi1i2CDkP9gb4Ig,96695
40
+ edda/viewer_ui/app.py,sha256=xZdIIGX5D2efNWQSVpPdldxLukHHpJD7JiAa_YKG5Uw,97084
41
41
  edda/viewer_ui/components.py,sha256=A0IxLwgj_Lu51O57OfzOwME8jzoJtKegEVvSnWc7uPo,45174
42
42
  edda/viewer_ui/data_service.py,sha256=KOqnWr-Y8seH_dkJH_ejHRfxQqn7aY8Ni5C54tx2Z-E,36621
43
43
  edda/viewer_ui/theme.py,sha256=mrXoXLRzgSnvE2a58LuMcPJkhlvHEDMWVa8Smqtk4l0,8118
@@ -47,8 +47,8 @@ edda/visualizer/mermaid_generator.py,sha256=XWa2egoOTNDfJEjPcwoxwQmblUqXf7YInWFj
47
47
  edda/migrations/mysql/20251217000000_initial_schema.sql,sha256=LpINasESRhadOeqABwDk4JZ0OZ4_zQw_opnhIR4Xe9U,12367
48
48
  edda/migrations/postgresql/20251217000000_initial_schema.sql,sha256=hCaGMWeptpzpnsjfNKVsMYuwPRe__fK9E0VZpClAumQ,11732
49
49
  edda/migrations/sqlite/20251217000000_initial_schema.sql,sha256=Wq9gCnQ0K9SOt0PY_8f1MG4va8rLVWIIcf2lnRzSK5g,11906
50
- edda_framework-0.12.0.dist-info/METADATA,sha256=5OyvWeuFkn7twSOODq7hUSzile0aMvOSgDhXshEsPF0,37567
51
- edda_framework-0.12.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
52
- edda_framework-0.12.0.dist-info/entry_points.txt,sha256=dPH47s6UoJgUZxHoeSMqZsQkLaSE-SGLi-gh88k2WrU,48
53
- edda_framework-0.12.0.dist-info/licenses/LICENSE,sha256=udxb-V7_cYKTHqW7lNm48rxJ-Zpf0WAY_PyGDK9BPCo,1069
54
- edda_framework-0.12.0.dist-info/RECORD,,
50
+ edda_framework-0.14.0.dist-info/METADATA,sha256=FbHEPrrr0THCfzFccY0_NJyCPuGArsxK6AKVrmTUKjQ,37567
51
+ edda_framework-0.14.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
52
+ edda_framework-0.14.0.dist-info/entry_points.txt,sha256=dPH47s6UoJgUZxHoeSMqZsQkLaSE-SGLi-gh88k2WrU,48
53
+ edda_framework-0.14.0.dist-info/licenses/LICENSE,sha256=udxb-V7_cYKTHqW7lNm48rxJ-Zpf0WAY_PyGDK9BPCo,1069
54
+ edda_framework-0.14.0.dist-info/RECORD,,