loom-core 0.1.4__py3-none-any.whl → 0.1.5__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.
loom/core/context.py CHANGED
@@ -102,6 +102,15 @@ class WorkflowContext(Generic[InputT, StateT]):
102
102
  """
103
103
  return self.cursor < len(self.history)
104
104
 
105
+ @property
106
+ def is_at_end_of_history(self) -> bool:
107
+ """Check if we've consumed all events in history.
108
+
109
+ Returns:
110
+ True if we're at the end of event history, False otherwise
111
+ """
112
+ return self.cursor >= len(self.history)
113
+
105
114
  def _extract_activity_metadata[FuncReturn](
106
115
  self, fn: Callable[..., Awaitable[FuncReturn]], args: tuple[Any, ...]
107
116
  ) -> ActivityMetadata:
loom/core/engine.py CHANGED
@@ -93,9 +93,42 @@ class Engine(Generic[InputT, StateT]):
93
93
 
94
94
  try:
95
95
  for step in steps:
96
+ # Check for and consume STEP_START event during replay
97
+ step_start_event = ctx._match_event("STEP_START")
98
+ if step_start_event:
99
+ if step_start_event["payload"]["step_name"] != step["name"]:
100
+ raise StopReplay # Step mismatch, something changed
101
+ ctx._consume()
102
+ else:
103
+ # Not replaying, emit STEP_START event
104
+ if not ctx.is_replaying:
105
+ await ctx._append_event(
106
+ "STEP_START",
107
+ {
108
+ "step_name": step["name"],
109
+ "step_fn": step["fn"],
110
+ "started_at": datetime.now(timezone.utc).isoformat(),
111
+ },
112
+ )
113
+
96
114
  step_fn = getattr(workflow, step["fn"])
97
115
  await step_fn(ctx)
98
116
 
117
+ # Check for and consume STEP_END event during replay
118
+ step_end_event = ctx._match_event("STEP_END")
119
+ if step_end_event:
120
+ ctx._consume()
121
+ else:
122
+ # Not replaying, emit STEP_END event
123
+ if not ctx.is_replaying:
124
+ await ctx._append_event(
125
+ "STEP_END",
126
+ {
127
+ "step_name": step["name"],
128
+ "completed_at": datetime.now(timezone.utc).isoformat(),
129
+ },
130
+ )
131
+
99
132
  except StopReplay:
100
133
  last = ctx.last_emitted_event_type()
101
134
  if last in ("STATE_SET", "STATE_UPDATE"):
loom/core/runner.py CHANGED
@@ -50,4 +50,13 @@ async def run_once() -> bool:
50
50
  except Exception as e:
51
51
  traceback.print_exc()
52
52
  await db.task_failed(task["id"], str(e))
53
+
54
+ # Mark workflow as failed when unhandled exception occurs
55
+ await db.workflow_failed(
56
+ workflow_id=workflow_id,
57
+ error=str(e),
58
+ task_id=task["id"],
59
+ task_kind=task["kind"],
60
+ )
61
+
53
62
  return True
loom/database/db.py CHANGED
@@ -344,6 +344,72 @@ class Database(Generic[InputT, StateT]):
344
344
  (workflow_id, json.dumps(signal_payload)),
345
345
  )
346
346
 
347
+ async def workflow_failed(
348
+ self, workflow_id: str, error: str, task_id: str = None, task_kind: str = None
349
+ ) -> None:
350
+ """Mark a workflow as failed due to an unhandled exception.
351
+
352
+ Creates a WORKFLOW_FAILED event and updates the workflow status.
353
+ Also cancels any remaining pending tasks for the workflow.
354
+
355
+ Args:
356
+ workflow_id: Workflow identifier to mark as failed
357
+ error: Error message describing the failure
358
+ task_id: Optional task ID that caused the failure
359
+ task_kind: Optional task kind that caused the failure
360
+
361
+ Raises:
362
+ WorkflowNotFoundError: If the workflow doesn't exist
363
+ """
364
+ # Get workflow info (this will raise WorkflowNotFoundError if not found)
365
+ workflow = await self.get_workflow_info(workflow_id)
366
+
367
+ # Skip if already in terminal state
368
+ if workflow["status"] in ("COMPLETED", "FAILED", "CANCELED"):
369
+ return
370
+
371
+ # Prepare failure payload
372
+ payload = {
373
+ "error": error,
374
+ "failed_at": datetime.now(timezone.utc).isoformat(),
375
+ }
376
+ if task_id:
377
+ payload["task_id"] = task_id
378
+ if task_kind:
379
+ payload["task_kind"] = task_kind
380
+
381
+ # Create failure event
382
+ await self.execute(
383
+ """
384
+ INSERT INTO events (workflow_id, type, payload)
385
+ VALUES (?, 'WORKFLOW_FAILED', ?)
386
+ """,
387
+ (workflow_id, json.dumps(payload)),
388
+ )
389
+
390
+ # Update workflow status
391
+ await self.execute(
392
+ """
393
+ UPDATE workflows
394
+ SET status = 'FAILED',
395
+ updated_at = CURRENT_TIMESTAMP
396
+ WHERE id = ?
397
+ """,
398
+ (workflow_id,),
399
+ )
400
+
401
+ # Cancel all pending tasks
402
+ await self.execute(
403
+ """
404
+ UPDATE tasks
405
+ SET status = 'FAILED',
406
+ last_error = 'workflow failed'
407
+ WHERE workflow_id = ?
408
+ AND status = 'PENDING'
409
+ """,
410
+ (workflow_id,),
411
+ )
412
+
347
413
  async def cancel_workflow(
348
414
  self, workflow_id: str, reason: str | None = None
349
415
  ) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loom-core
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Durable workflow orchestration engine for Python
5
5
  Home-page: https://github.com/satadeep3927/loom
6
6
  Author: Satadeep Dasgupta
@@ -7,16 +7,16 @@ loom/common/errors.py,sha256=jc7_XS8dUPS8fg5pb7kJaTuIR1e6eKBZyZtbsHW3lgM,1712
7
7
  loom/common/workflow.py,sha256=R4X1vt6duqTJz6kGk6YI866O-CleOiyw0dXdUVo_qAE,2118
8
8
  loom/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  loom/core/compiled.py,sha256=Y2NQDMZVLee65ILgJoK2Ny_OcR1T4LJnOQWQcyg0Z7U,1178
10
- loom/core/context.py,sha256=rVWWSOaAmdVOzKKFh9TwpJVX-4Rm0h7bC76ipEcqVBw,9219
11
- loom/core/engine.py,sha256=twrZ8bABmQS7aVrj6qObPvx4zxhQTTFm2LY-KyGrhYs,4268
10
+ loom/core/context.py,sha256=7zezfvPaPdyfjJnUd8cjTwoxKGUh0N6F1nHn8dCuUVY,9494
11
+ loom/core/engine.py,sha256=6EvacrZCfVhJQ7yPurRdCeTzlGRaGk5uI-s7yxu9IUs,5822
12
12
  loom/core/handle.py,sha256=beOVmJit-gSrk7II4c2KuSubmr5OIPls82_vJ-pI04U,5532
13
13
  loom/core/logger.py,sha256=4ej95IdHOdBh-y7PQoRFz_H22PJYDXvAioBoId8mOhg,1729
14
- loom/core/runner.py,sha256=pYITY35Q88ItZA4EuSWmNE5sgxL0tExXrm9j5vSaz6o,1718
14
+ loom/core/runner.py,sha256=nNA-EcdJt3UEgwzKHyb22xD7aLT8kI01x5UVdLteSI8,1998
15
15
  loom/core/state.py,sha256=oXiZ442PL2kl_JyQXLZOFeBLs_cTF2iEjykKVVUNiAI,2997
16
16
  loom/core/worker.py,sha256=jpTtvM1rIToVkG4SwJu1W9Q-eL3bw8b3N8KsxxrRovA,5322
17
17
  loom/core/workflow.py,sha256=E_A2rI6iaW3liZmVy3X98oxSeBDXMGiQnRZRq3Cp3BA,5755
18
18
  loom/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- loom/database/db.py,sha256=BtZTNtZ6cXpfvrtdmW8Xb47zoC2NXvVoWVmUILow2eA,23535
19
+ loom/database/db.py,sha256=MSNXWhUzfwvjPXaVWY3-UK1lqw-Qas0Cc1FYcU8qdWw,25700
20
20
  loom/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  loom/decorators/activity.py,sha256=xt7oeYOKJxhKIEcwWsSVA26c_jk4yQB1PHtbcg3tCFI,4824
22
22
  loom/decorators/workflow.py,sha256=rBpbnpLfDTTAWdgiERpUPIgNUJpnYJKvBZPmzx-ECcc,1516
@@ -42,9 +42,9 @@ loom/schemas/database.py,sha256=DlMBvacsJ9oceBHkrMiJC7uD3_rCHFe6Kwa2STFcKOw,283
42
42
  loom/schemas/events.py,sha256=Gz-R836nVXNSsp4Y54idhKs_WTvzFPmx5KlA8PunP28,2216
43
43
  loom/schemas/tasks.py,sha256=DJbLInggIGxI-CfP1kSyK79Bz83e3sZyEKh6C2HE8q4,1341
44
44
  loom/schemas/workflow.py,sha256=F1cNEJRuEWLjPhGinEZE2T6vwchDC8RuTHmxEIaH6Ls,671
45
- loom_core-0.1.4.dist-info/licenses/LICENSE,sha256=8EpC-clAYRUfJQ92T3iQEIIWYjx2A3Kfk28zOd8lh7I,1095
46
- loom_core-0.1.4.dist-info/METADATA,sha256=pRtqEJf1DiwSbXPfnE-cHfuxZs8t2ol1I4LiXfAnm50,8897
47
- loom_core-0.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
48
- loom_core-0.1.4.dist-info/entry_points.txt,sha256=Jx5HXHL2y7jvSjkwkH3QqF954cbSxiE6OGwL2coldyE,42
49
- loom_core-0.1.4.dist-info/top_level.txt,sha256=cAfRgAuCuit-cU9iBrf0bS4ovvmq-URykNd9fmYMojg,5
50
- loom_core-0.1.4.dist-info/RECORD,,
45
+ loom_core-0.1.5.dist-info/licenses/LICENSE,sha256=8EpC-clAYRUfJQ92T3iQEIIWYjx2A3Kfk28zOd8lh7I,1095
46
+ loom_core-0.1.5.dist-info/METADATA,sha256=TdEYynBj66M4a173vlAyY5YAteLmBNVKEiqSuWZXx3Q,8897
47
+ loom_core-0.1.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
48
+ loom_core-0.1.5.dist-info/entry_points.txt,sha256=Jx5HXHL2y7jvSjkwkH3QqF954cbSxiE6OGwL2coldyE,42
49
+ loom_core-0.1.5.dist-info/top_level.txt,sha256=cAfRgAuCuit-cU9iBrf0bS4ovvmq-URykNd9fmYMojg,5
50
+ loom_core-0.1.5.dist-info/RECORD,,