loom-core 0.1.4__tar.gz → 0.1.5__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 (58) hide show
  1. {loom_core-0.1.4 → loom_core-0.1.5}/PKG-INFO +1 -1
  2. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/context.py +9 -0
  3. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/engine.py +33 -0
  4. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/runner.py +9 -0
  5. {loom_core-0.1.4 → loom_core-0.1.5}/loom/database/db.py +66 -0
  6. {loom_core-0.1.4 → loom_core-0.1.5}/loom_core.egg-info/PKG-INFO +1 -1
  7. {loom_core-0.1.4 → loom_core-0.1.5}/pyproject.toml +1 -1
  8. {loom_core-0.1.4 → loom_core-0.1.5}/LICENSE +0 -0
  9. {loom_core-0.1.4 → loom_core-0.1.5}/MANIFEST.in +0 -0
  10. {loom_core-0.1.4 → loom_core-0.1.5}/QUICKSTART.md +0 -0
  11. {loom_core-0.1.4 → loom_core-0.1.5}/README.md +0 -0
  12. {loom_core-0.1.4 → loom_core-0.1.5}/loom/__init__.py +0 -0
  13. {loom_core-0.1.4 → loom_core-0.1.5}/loom/cli/__init__.py +0 -0
  14. {loom_core-0.1.4 → loom_core-0.1.5}/loom/cli/cli.py +0 -0
  15. {loom_core-0.1.4 → loom_core-0.1.5}/loom/common/activity.py +0 -0
  16. {loom_core-0.1.4 → loom_core-0.1.5}/loom/common/config.py +0 -0
  17. {loom_core-0.1.4 → loom_core-0.1.5}/loom/common/errors.py +0 -0
  18. {loom_core-0.1.4 → loom_core-0.1.5}/loom/common/workflow.py +0 -0
  19. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/__init__.py +0 -0
  20. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/compiled.py +0 -0
  21. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/handle.py +0 -0
  22. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/logger.py +0 -0
  23. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/state.py +0 -0
  24. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/worker.py +0 -0
  25. {loom_core-0.1.4 → loom_core-0.1.5}/loom/core/workflow.py +0 -0
  26. {loom_core-0.1.4 → loom_core-0.1.5}/loom/database/__init__.py +0 -0
  27. {loom_core-0.1.4 → loom_core-0.1.5}/loom/decorators/__init__.py +0 -0
  28. {loom_core-0.1.4 → loom_core-0.1.5}/loom/decorators/activity.py +0 -0
  29. {loom_core-0.1.4 → loom_core-0.1.5}/loom/decorators/workflow.py +0 -0
  30. {loom_core-0.1.4 → loom_core-0.1.5}/loom/lib/progress.py +0 -0
  31. {loom_core-0.1.4 → loom_core-0.1.5}/loom/lib/utils.py +0 -0
  32. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/down/001_setup_pragma.sql +0 -0
  33. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/down/002_create_workflows.sql +0 -0
  34. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/down/003.create_events.sql +0 -0
  35. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/down/004.create_tasks.sql +0 -0
  36. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/down/005.create_indexes.sql +0 -0
  37. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/down/006_auto_update_triggers.sql +0 -0
  38. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/down/007_create_logs.sql +0 -0
  39. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/up/001_setup_pragma.sql +0 -0
  40. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/up/002_create_workflows.sql +0 -0
  41. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/up/003_create_events.sql +0 -0
  42. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/up/004_create_tasks.sql +0 -0
  43. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/up/005_create_indexes.sql +0 -0
  44. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/up/006_auto_update_triggers.sql +0 -0
  45. {loom_core-0.1.4 → loom_core-0.1.5}/loom/migrations/up/007_create_logs.sql +0 -0
  46. {loom_core-0.1.4 → loom_core-0.1.5}/loom/schemas/__init__.py +0 -0
  47. {loom_core-0.1.4 → loom_core-0.1.5}/loom/schemas/activity.py +0 -0
  48. {loom_core-0.1.4 → loom_core-0.1.5}/loom/schemas/database.py +0 -0
  49. {loom_core-0.1.4 → loom_core-0.1.5}/loom/schemas/events.py +0 -0
  50. {loom_core-0.1.4 → loom_core-0.1.5}/loom/schemas/tasks.py +0 -0
  51. {loom_core-0.1.4 → loom_core-0.1.5}/loom/schemas/workflow.py +0 -0
  52. {loom_core-0.1.4 → loom_core-0.1.5}/loom_core.egg-info/SOURCES.txt +0 -0
  53. {loom_core-0.1.4 → loom_core-0.1.5}/loom_core.egg-info/dependency_links.txt +0 -0
  54. {loom_core-0.1.4 → loom_core-0.1.5}/loom_core.egg-info/entry_points.txt +0 -0
  55. {loom_core-0.1.4 → loom_core-0.1.5}/loom_core.egg-info/requires.txt +0 -0
  56. {loom_core-0.1.4 → loom_core-0.1.5}/loom_core.egg-info/top_level.txt +0 -0
  57. {loom_core-0.1.4 → loom_core-0.1.5}/setup.cfg +0 -0
  58. {loom_core-0.1.4 → loom_core-0.1.5}/setup.py +0 -0
@@ -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
@@ -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:
@@ -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"):
@@ -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
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "loom-core"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "Durable workflow orchestration engine for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes