loom-core 0.1.3__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.3 → loom_core-0.1.5}/PKG-INFO +1 -1
  2. {loom_core-0.1.3 → loom_core-0.1.5}/loom/cli/cli.py +1 -1
  3. {loom_core-0.1.3 → loom_core-0.1.5}/loom/common/activity.py +6 -0
  4. {loom_core-0.1.3 → loom_core-0.1.5}/loom/common/workflow.py +6 -0
  5. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/context.py +9 -0
  6. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/engine.py +36 -1
  7. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/runner.py +9 -0
  8. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/state.py +1 -1
  9. {loom_core-0.1.3 → loom_core-0.1.5}/loom/database/db.py +66 -0
  10. {loom_core-0.1.3 → loom_core-0.1.5}/loom_core.egg-info/PKG-INFO +1 -1
  11. {loom_core-0.1.3 → loom_core-0.1.5}/pyproject.toml +1 -1
  12. {loom_core-0.1.3 → loom_core-0.1.5}/LICENSE +0 -0
  13. {loom_core-0.1.3 → loom_core-0.1.5}/MANIFEST.in +0 -0
  14. {loom_core-0.1.3 → loom_core-0.1.5}/QUICKSTART.md +0 -0
  15. {loom_core-0.1.3 → loom_core-0.1.5}/README.md +0 -0
  16. {loom_core-0.1.3 → loom_core-0.1.5}/loom/__init__.py +0 -0
  17. {loom_core-0.1.3 → loom_core-0.1.5}/loom/cli/__init__.py +0 -0
  18. {loom_core-0.1.3 → loom_core-0.1.5}/loom/common/config.py +0 -0
  19. {loom_core-0.1.3 → loom_core-0.1.5}/loom/common/errors.py +0 -0
  20. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/__init__.py +0 -0
  21. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/compiled.py +0 -0
  22. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/handle.py +0 -0
  23. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/logger.py +0 -0
  24. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/worker.py +0 -0
  25. {loom_core-0.1.3 → loom_core-0.1.5}/loom/core/workflow.py +0 -0
  26. {loom_core-0.1.3 → loom_core-0.1.5}/loom/database/__init__.py +0 -0
  27. {loom_core-0.1.3 → loom_core-0.1.5}/loom/decorators/__init__.py +0 -0
  28. {loom_core-0.1.3 → loom_core-0.1.5}/loom/decorators/activity.py +0 -0
  29. {loom_core-0.1.3 → loom_core-0.1.5}/loom/decorators/workflow.py +0 -0
  30. {loom_core-0.1.3 → loom_core-0.1.5}/loom/lib/progress.py +0 -0
  31. {loom_core-0.1.3 → loom_core-0.1.5}/loom/lib/utils.py +0 -0
  32. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/down/001_setup_pragma.sql +0 -0
  33. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/down/002_create_workflows.sql +0 -0
  34. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/down/003.create_events.sql +0 -0
  35. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/down/004.create_tasks.sql +0 -0
  36. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/down/005.create_indexes.sql +0 -0
  37. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/down/006_auto_update_triggers.sql +0 -0
  38. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/down/007_create_logs.sql +0 -0
  39. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/up/001_setup_pragma.sql +0 -0
  40. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/up/002_create_workflows.sql +0 -0
  41. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/up/003_create_events.sql +0 -0
  42. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/up/004_create_tasks.sql +0 -0
  43. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/up/005_create_indexes.sql +0 -0
  44. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/up/006_auto_update_triggers.sql +0 -0
  45. {loom_core-0.1.3 → loom_core-0.1.5}/loom/migrations/up/007_create_logs.sql +0 -0
  46. {loom_core-0.1.3 → loom_core-0.1.5}/loom/schemas/__init__.py +0 -0
  47. {loom_core-0.1.3 → loom_core-0.1.5}/loom/schemas/activity.py +0 -0
  48. {loom_core-0.1.3 → loom_core-0.1.5}/loom/schemas/database.py +0 -0
  49. {loom_core-0.1.3 → loom_core-0.1.5}/loom/schemas/events.py +0 -0
  50. {loom_core-0.1.3 → loom_core-0.1.5}/loom/schemas/tasks.py +0 -0
  51. {loom_core-0.1.3 → loom_core-0.1.5}/loom/schemas/workflow.py +0 -0
  52. {loom_core-0.1.3 → loom_core-0.1.5}/loom_core.egg-info/SOURCES.txt +0 -0
  53. {loom_core-0.1.3 → loom_core-0.1.5}/loom_core.egg-info/dependency_links.txt +0 -0
  54. {loom_core-0.1.3 → loom_core-0.1.5}/loom_core.egg-info/entry_points.txt +0 -0
  55. {loom_core-0.1.3 → loom_core-0.1.5}/loom_core.egg-info/requires.txt +0 -0
  56. {loom_core-0.1.3 → loom_core-0.1.5}/loom_core.egg-info/top_level.txt +0 -0
  57. {loom_core-0.1.3 → loom_core-0.1.5}/setup.cfg +0 -0
  58. {loom_core-0.1.3 → 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.3
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
@@ -2,8 +2,8 @@
2
2
  """Loom CLI - Command-line interface for Loom workflow orchestration."""
3
3
 
4
4
  import asyncio
5
- import sys
6
5
  import os # Added import
6
+ import sys
7
7
  from typing import Any
8
8
 
9
9
  import click
@@ -1,4 +1,5 @@
1
1
  import importlib
2
+ import sys
2
3
  from typing import Any, Awaitable, Callable, cast
3
4
 
4
5
 
@@ -6,6 +7,11 @@ def load_activity(module: str, func: str) -> Callable[..., Awaitable[Any]]:
6
7
 
7
8
  try:
8
9
  activity_module = importlib.import_module(module)
10
+
11
+ # Force reload the module to get latest changes
12
+ if module in sys.modules:
13
+ activity_module = importlib.reload(activity_module)
14
+
9
15
  except ModuleNotFoundError as e:
10
16
  raise ModuleNotFoundError(
11
17
  f"Cannot import activity module '{module}'. "
@@ -1,4 +1,5 @@
1
1
  import importlib
2
+ import sys
2
3
  from typing import Any, Type
3
4
 
4
5
  from ..core.workflow import Workflow
@@ -32,6 +33,11 @@ def workflow_registry(module: str, cls: str) -> Type[Workflow[Any, Any]]:
32
33
  """
33
34
  try:
34
35
  workflow_module = importlib.import_module(module)
36
+
37
+ # Force reload the module to get latest changes
38
+ if module in sys.modules:
39
+ workflow_module = importlib.reload(workflow_module)
40
+
35
41
  except ModuleNotFoundError as e:
36
42
  raise ModuleNotFoundError(
37
43
  f"Cannot import workflow module '{module}'. "
@@ -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:
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from datetime import datetime, timedelta, timezone
2
3
  from typing import Generic
3
4
 
@@ -76,9 +77,10 @@ class Engine(Generic[InputT, StateT]):
76
77
  async with Database[InputT, StateT]() as db:
77
78
  workflow_def = await db.get_workflow_info(workflow_id)
78
79
  history = await db.get_workflow_events(workflow_id=workflow_def["id"])
80
+ workflow_input = json.loads(workflow_def["input"])
79
81
 
80
82
  ctx: WorkflowContext = WorkflowContext(
81
- workflow_def["id"], workflow_def["input"], history, {}
83
+ workflow_def["id"], workflow_input, history, {}
82
84
  )
83
85
 
84
86
  first_event = ctx._peek()
@@ -91,9 +93,42 @@ class Engine(Generic[InputT, StateT]):
91
93
 
92
94
  try:
93
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
+
94
114
  step_fn = getattr(workflow, step["fn"])
95
115
  await step_fn(ctx)
96
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
+
97
132
  except StopReplay:
98
133
  last = ctx.last_emitted_event_type()
99
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
@@ -85,7 +85,7 @@ class StateProxy(Generic[InputT, StateT]):
85
85
  """
86
86
  if self._batch is not None:
87
87
  raise RuntimeError("Nested batches are not supported.")
88
- self._batch = [] # type: ignore
88
+ self._batch = [] # type: ignore
89
89
 
90
90
  try:
91
91
  yield
@@ -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.3
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.3"
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