glyff 0.1.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.
glyff/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .decorators import engrave
2
+ from .interfaces import ArgsHasher, Serializer, SessionStore
3
+ from .models import ExecutionId, ExecutionRecord, ExecutionStatus
4
+ from .session import Session
5
+
6
+ __all__ = [
7
+ "engrave",
8
+ "Session",
9
+ "ExecutionId",
10
+ "ExecutionRecord",
11
+ "ExecutionStatus",
12
+ "ArgsHasher",
13
+ "Serializer",
14
+ "SessionStore",
15
+ ]
glyff/context.py ADDED
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ import contextvars
4
+ from collections.abc import Iterator, Sequence
5
+ from typing import Callable, overload
6
+
7
+ from .exceptions import ExecutionFailedError, YieldException
8
+ from .interfaces import ArgsHasher, SessionStore, Transaction
9
+ from .models import ExecutionId
10
+ from .sequencer import Sequencer
11
+
12
+
13
+ class Context:
14
+ """Holds the execution context for a workflow session."""
15
+
16
+ def __init__(
17
+ self,
18
+ session_id: str,
19
+ store: SessionStore,
20
+ sequencer: Sequencer,
21
+ hasher: ArgsHasher,
22
+ transaction_scope_factory: Callable[[], TransactionScope],
23
+ ) -> None:
24
+ self._session_id = session_id
25
+ self._store = store
26
+ self._sequencer = sequencer
27
+ self._hasher = hasher
28
+ self._transaction_scope_factory = transaction_scope_factory
29
+ self._tracer = ExecutionTracer()
30
+ self._current_transaction_scope: TransactionScope | None = None
31
+
32
+ @property
33
+ def store(self) -> SessionStore:
34
+ return self._store
35
+
36
+ @property
37
+ def sequencer(self) -> Sequencer:
38
+ return self._sequencer
39
+
40
+ @property
41
+ def hasher(self) -> ArgsHasher:
42
+ return self._hasher
43
+
44
+ @property
45
+ def tracer(self) -> ExecutionTracer:
46
+ return self._tracer
47
+
48
+ @property
49
+ def call_stack(self) -> CallStack:
50
+ return self._tracer.call_stack
51
+
52
+ @property
53
+ def current_execution_id(self) -> ExecutionId | None:
54
+ return self._tracer.current
55
+
56
+ @property
57
+ def in_transaction(self) -> bool:
58
+ """Returns True if currently within a transaction scope."""
59
+ ts = self._current_transaction_scope
60
+ return ts is not None and ts.in_transaction
61
+
62
+ def get_transaction_scope(self) -> TransactionScope:
63
+ if self._current_transaction_scope is None:
64
+ self._current_transaction_scope = self._transaction_scope_factory()
65
+ return self._current_transaction_scope
66
+
67
+
68
+ class CallStack(Sequence[ExecutionId]):
69
+ """Read-only view of the execution call stack. No allocation on access."""
70
+
71
+ __slots__ = ("_data",)
72
+
73
+ def __init__(self, data: list[ExecutionId]) -> None:
74
+ self._data = data
75
+
76
+ @overload
77
+ def __getitem__(self, index: int) -> ExecutionId: ...
78
+ @overload
79
+ def __getitem__(self, index: slice) -> list[ExecutionId]: ...
80
+
81
+ def __getitem__(self, index):
82
+ return self._data[index]
83
+
84
+ def __len__(self) -> int:
85
+ return len(self._data)
86
+
87
+ def __contains__(self, item: object) -> bool:
88
+ return item in self._data
89
+
90
+ def __iter__(self) -> Iterator[ExecutionId]:
91
+ return iter(self._data)
92
+
93
+ def __repr__(self) -> str:
94
+ return f"CallStack({self._data!r})"
95
+
96
+
97
+ class ExecutionTracer:
98
+ """Records the active call stack during workflow execution."""
99
+
100
+ def __init__(self) -> None:
101
+ self._stack: list[ExecutionId] = []
102
+ self._view = CallStack(self._stack)
103
+
104
+ @property
105
+ def call_stack(self) -> CallStack:
106
+ return self._view
107
+
108
+ @property
109
+ def current(self) -> ExecutionId | None:
110
+ return self._stack[-1] if self._stack else None
111
+
112
+ def start(self, execution_id: ExecutionId) -> None:
113
+ self._stack.append(execution_id)
114
+
115
+ def end(self) -> None:
116
+ self._stack.pop()
117
+
118
+
119
+ class TransactionScope:
120
+ """
121
+ Manages a transaction across a SessionStore, supporting nesting.
122
+ The actual commit/rollback only happens at the outermost scope.
123
+ """
124
+
125
+ def __init__(self, store: SessionStore):
126
+ self._store = store
127
+ self._level = 0
128
+ self._transaction: Transaction | None = None
129
+
130
+ @property
131
+ def in_transaction(self) -> bool:
132
+ """Returns True if currently within a transaction scope."""
133
+ return self._level > 0
134
+
135
+ async def __aenter__(self):
136
+ if self._level == 0:
137
+ self._transaction = await self._store.begin_transaction()
138
+ self._level += 1
139
+ return self
140
+
141
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
142
+ self._level -= 1
143
+ if self._level == 0 and self._transaction:
144
+ if exc_type is None or isinstance(
145
+ exc_val, (YieldException, ExecutionFailedError)
146
+ ):
147
+ # On YieldException or ExecutionFailedError we still commit so that
148
+ # state (completed subtasks or the failure record) is durably saved.
149
+ await self._transaction.commit()
150
+ else:
151
+ await self._transaction.rollback()
152
+
153
+
154
+ _context_var: contextvars.ContextVar[Context] = contextvars.ContextVar("glyff_context")
155
+
156
+
157
+ def get_context() -> Context:
158
+ """Retrieves the current workflow context."""
159
+ try:
160
+ return _context_var.get()
161
+ except LookupError:
162
+ raise RuntimeError(
163
+ "Workflow context is not set. Are you running outside a Session?"
164
+ )
165
+
166
+
167
+ def set_context(ctx: Context) -> contextvars.Token:
168
+ """Sets the current workflow context. Returns a token that can be used to reset it."""
169
+ return _context_var.set(ctx)
170
+
171
+
172
+ def reset_context(token: contextvars.Token) -> None:
173
+ """Resets the workflow context to a previous state using the provided token."""
174
+ _context_var.reset(token)
glyff/decorators.py ADDED
@@ -0,0 +1,51 @@
1
+ import functools
2
+ import inspect
3
+ from typing import Any, Callable, ParamSpec, TypeVar, cast
4
+
5
+ from .context import get_context
6
+ from .executor import execute
7
+ from .models import ExecutionId
8
+
9
+ P = ParamSpec("P")
10
+ R = TypeVar("R")
11
+
12
+
13
+ def engrave(func: Callable[P, R]) -> Callable[P, R]:
14
+ """
15
+ Decorator that makes an async method engraveable and resumable.
16
+ Its main responsibilities are ExecutionId creation and delegation to the
17
+ `executor` module.
18
+ """
19
+ sig = inspect.signature(func)
20
+ task_name = getattr(func, "__qualname__", func.__name__)
21
+
22
+ try:
23
+ type_hints = inspect.get_annotations(func, eval_str=True)
24
+ return_type = type_hints.get("return", Any)
25
+ except Exception as e:
26
+ raise TypeError(
27
+ f"Could not resolve type hints for {task_name}. "
28
+ f"Please ensure all types are correctly defined and imported. Error: {e}"
29
+ ) from e
30
+
31
+ @functools.wraps(func)
32
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
33
+ ctx = get_context()
34
+ parent_id = ctx.current_execution_id
35
+ seq = await ctx.sequencer.next(parent_id, task_name)
36
+ args_hash = ctx.hasher.hash_args(func, sig, args, kwargs)
37
+ execution_id = ExecutionId(
38
+ parent_id=parent_id, name=task_name, sequence=seq, args_hash=args_hash
39
+ )
40
+
41
+ result = await execute(
42
+ ctx=ctx,
43
+ execution_id=execution_id,
44
+ func=func,
45
+ args=args,
46
+ kwargs=kwargs,
47
+ return_type=return_type,
48
+ )
49
+ return cast(R, result)
50
+
51
+ return cast(Callable[P, R], wrapper)
glyff/exceptions.py ADDED
@@ -0,0 +1,16 @@
1
+ class YieldException(Exception):
2
+ """
3
+ A special exception to signal that the session should be interrupted gracefully.
4
+ This is not an error, but a signal to stop processing and engrave the state.
5
+ """
6
+
7
+ pass
8
+
9
+
10
+ class ExecutionFailedError(Exception):
11
+ """
12
+ Raised when attempting to execute a task that has previously failed
13
+ and its failure state is engraved.
14
+ """
15
+
16
+ pass
glyff/executor.py ADDED
@@ -0,0 +1,60 @@
1
+ import traceback
2
+ from typing import Any, Callable
3
+
4
+ from .context import Context
5
+ from .exceptions import ExecutionFailedError, YieldException
6
+ from .models import ExecutionStatus
7
+
8
+
9
+ async def execute(
10
+ ctx: Context,
11
+ execution_id: Any,
12
+ func: Callable[..., Any],
13
+ args: tuple[Any, ...],
14
+ kwargs: dict[str, Any],
15
+ return_type: type,
16
+ ) -> Any:
17
+ """
18
+ Orchestrates the execution of a task, including cache checks, transaction
19
+ management, state recording, and exception handling.
20
+ """
21
+ store = ctx.store
22
+ sequencer = ctx.sequencer
23
+ tracer = ctx.tracer
24
+
25
+ record = await store.get_execution_record(execution_id, return_type)
26
+
27
+ if record:
28
+ if record.status == ExecutionStatus.COMPLETED:
29
+ return record.result
30
+ if record.status == ExecutionStatus.FAILED:
31
+ original_error = Exception(
32
+ record.error or "Unknown previously failed error"
33
+ )
34
+ raise ExecutionFailedError(
35
+ f"Task {execution_id} failed previously and cannot be re-executed."
36
+ ) from original_error
37
+
38
+ async with ctx.get_transaction_scope():
39
+ # Reset child sequencers for deterministic re-execution.
40
+ await sequencer.reset_for_call(execution_id)
41
+
42
+ execution = await store.start_execution(execution_id)
43
+ tracer.start(execution_id)
44
+
45
+ try:
46
+ result = await func(*args, **kwargs)
47
+ await execution.complete(result, return_type)
48
+ return result
49
+ except YieldException:
50
+ # Interruption is a graceful exit; don't stage failure.
51
+ # The state remains STARTED, allowing for resumption.
52
+ raise
53
+ except Exception as e:
54
+ error_str = "".join(traceback.format_exception(type(e), e, e.__traceback__))
55
+ await execution.fail(error_str)
56
+ raise ExecutionFailedError(
57
+ f"Task {execution_id} failed: {type(e).__name__}({e})"
58
+ ) from e
59
+ finally:
60
+ tracer.end()
glyff/interfaces.py ADDED
@@ -0,0 +1,92 @@
1
+ import inspect
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Callable
4
+
5
+ from .models import ExecutionId, ExecutionRecord
6
+
7
+
8
+ class Transaction(ABC):
9
+ """
10
+ A transaction context for a SessionStore.
11
+ Actual commit/rollback logic is delegated to this object.
12
+ """
13
+
14
+ @abstractmethod
15
+ async def commit(self) -> None:
16
+ """Commits the transaction."""
17
+ ...
18
+
19
+ @abstractmethod
20
+ async def rollback(self) -> None:
21
+ """Rolls back the transaction."""
22
+ ...
23
+
24
+
25
+ class Execution(ABC):
26
+ """
27
+ Represents a single task execution, handling its outcome.
28
+ """
29
+
30
+ @abstractmethod
31
+ async def complete(self, value: Any, return_type: type) -> None:
32
+ """Marks the task as successfully completed with a result."""
33
+ ...
34
+
35
+ @abstractmethod
36
+ async def fail(self, error: str) -> None:
37
+ """Marks the task as failed with an error message."""
38
+ ...
39
+
40
+
41
+ class Serializer(ABC):
42
+ """An interface for serializing/deserializing values."""
43
+
44
+ @abstractmethod
45
+ def serialize(self, value: Any, type_hint: type) -> bytes:
46
+ """Serializes a value to bytes."""
47
+ ...
48
+
49
+ @abstractmethod
50
+ def deserialize(self, data: bytes, type_hint: type) -> Any:
51
+ """Deserializes bytes to a value of the given type."""
52
+ ...
53
+
54
+
55
+ class ArgsHasher(ABC):
56
+ """An interface for creating a deterministic hash from function arguments."""
57
+
58
+ @abstractmethod
59
+ def hash_args(
60
+ self, func: Callable, sig: inspect.Signature, args: tuple, kwargs: dict
61
+ ) -> str:
62
+ """Creates a deterministic hash from a function's arguments."""
63
+ ...
64
+
65
+
66
+ class SessionStore(ABC):
67
+ """
68
+ Protocol for a store that persists the state and results of task calls.
69
+ """
70
+
71
+ @abstractmethod
72
+ async def begin_transaction(self) -> Transaction:
73
+ """Begins a transaction and returns a transaction object."""
74
+ ...
75
+
76
+ @abstractmethod
77
+ async def start_execution(self, execution_id: ExecutionId) -> Execution:
78
+ """
79
+ Records that a task has started and returns an execution object
80
+ to manage its outcome.
81
+ """
82
+ ...
83
+
84
+ @abstractmethod
85
+ async def get_execution_record(
86
+ self, execution_id: ExecutionId, return_type: type
87
+ ) -> ExecutionRecord | None:
88
+ """
89
+ Gets the persisted state of a task.
90
+ The result, if any, is deserialized to the given type.
91
+ """
92
+ ...
glyff/models.py ADDED
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum, auto
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ExecutionId:
10
+ """
11
+ A unique, deterministic identifier for a task call.
12
+ It forms a hierarchy through the 'parent_id' attribute.
13
+ """
14
+
15
+ parent_id: ExecutionId | None
16
+ name: str
17
+ sequence: int
18
+ args_hash: str
19
+
20
+ def __str__(self) -> str:
21
+ """
22
+ Generates a human-readable representation for debugging purposes only.
23
+ This format is NOT guaranteed to be stable or suitable for use as a persistence key.
24
+ """
25
+ parent_info = (
26
+ f", parent='{self.parent_id.name}#{self.parent_id.sequence}'"
27
+ if self.parent_id
28
+ else ""
29
+ )
30
+ return f"ExecutionId(name='{self.name}', sequence={self.sequence}{parent_info})"
31
+
32
+
33
+ class ExecutionStatus(Enum):
34
+ """Represents the lifecycle state of a task."""
35
+
36
+ STARTED = auto()
37
+ COMPLETED = auto()
38
+ FAILED = auto()
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ExecutionRecord:
43
+ """Represents the persisted state and outcome of a single execution."""
44
+
45
+ status: ExecutionStatus
46
+ result: Any | None = None
47
+ error: str | None = None
glyff/sequencer.py ADDED
@@ -0,0 +1,40 @@
1
+ import asyncio
2
+ from collections import defaultdict
3
+
4
+ from .models import ExecutionId
5
+
6
+
7
+ class Sequencer:
8
+ """
9
+ Generates sequential integers for ExecutionIds in a concurrency-safe manner.
10
+ Each (parent_id, name) pair has its own independent sequence.
11
+ """
12
+
13
+ def __init__(self):
14
+ self._locks: dict[tuple[ExecutionId | None, str], asyncio.Lock] = {}
15
+ self._counters: dict[tuple[ExecutionId | None, str], int] = defaultdict(int)
16
+ self._meta_lock = asyncio.Lock()
17
+
18
+ async def next(self, parent: ExecutionId | None, name: str) -> int:
19
+ """Returns the next sequence number for the given scope."""
20
+ key = (parent, name)
21
+
22
+ async with self._meta_lock:
23
+ if key not in self._locks:
24
+ self._locks[key] = asyncio.Lock()
25
+
26
+ async with self._locks[key]:
27
+ seq = self._counters[key]
28
+ self._counters[key] += 1
29
+ return seq
30
+
31
+ async def reset_for_call(self, execution_id: ExecutionId) -> None:
32
+ """
33
+ Resets all counters that are children of the given ExecutionId.
34
+ This is crucial for deterministic re-execution of a parent task.
35
+ """
36
+ async with self._meta_lock:
37
+ keys_to_reset = [key for key in self._counters if key[0] == execution_id]
38
+ for key in keys_to_reset:
39
+ del self._counters[key]
40
+ del self._locks[key]
@@ -0,0 +1,6 @@
1
+ from .json import JsonArgsHasher, JsonSerializer
2
+
3
+ __all__ = [
4
+ "JsonSerializer",
5
+ "JsonArgsHasher",
6
+ ]
@@ -0,0 +1,48 @@
1
+ import hashlib
2
+ import inspect
3
+ import json
4
+ from typing import Any, Callable
5
+
6
+ from ..interfaces import ArgsHasher, Serializer
7
+
8
+
9
+ def _json_stable_dumps(data: Any) -> str:
10
+ return json.dumps(data, sort_keys=True, separators=(",", ":"))
11
+
12
+
13
+ class JsonSerializer(Serializer):
14
+ """A serializer using only the standard `json` module."""
15
+
16
+ def serialize(self, value: Any, type_hint: type) -> bytes:
17
+ return _json_stable_dumps(value).encode("utf-8")
18
+
19
+ def deserialize(self, data: bytes, type_hint: type) -> Any:
20
+ return json.loads(data.decode("utf-8"))
21
+
22
+
23
+ class JsonArgsHasher(ArgsHasher):
24
+ """An ArgsHasher using standard JSON serialization."""
25
+
26
+ def hash_args(
27
+ self, func: Callable, sig: inspect.Signature, args: tuple, kwargs: dict
28
+ ) -> str:
29
+ bound = sig.bind(*args, **kwargs)
30
+ bound.apply_defaults()
31
+ args_dict = {
32
+ name: value
33
+ for name, value in bound.arguments.items()
34
+ if sig.parameters[name].kind
35
+ not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
36
+ and name not in ("self", "cls")
37
+ }
38
+ try:
39
+ stable_repr = _json_stable_dumps(args_dict)
40
+ except TypeError as e:
41
+ raise TypeError(
42
+ f"Arguments to '{func.__name__}' could not be serialized to JSON. "
43
+ "Ensure all arguments are JSON-serializable. "
44
+ f"Original error: {e}"
45
+ ) from e
46
+ hasher = hashlib.sha256()
47
+ hasher.update(stable_repr.encode("utf-8"))
48
+ return hasher.hexdigest()
glyff/session.py ADDED
@@ -0,0 +1,52 @@
1
+ from .context import Context, TransactionScope, reset_context, set_context
2
+ from .interfaces import ArgsHasher, SessionStore
3
+ from .sequencer import Sequencer
4
+
5
+
6
+ class Session:
7
+ """
8
+ Manages the lifecycle of a workflow execution.
9
+ It sets up the execution context and the top-level transaction.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ id: str,
15
+ store: SessionStore,
16
+ hasher: ArgsHasher,
17
+ ):
18
+ self._id = id
19
+ self._store = store
20
+ self._hasher = hasher
21
+ self._context: Context | None = None
22
+ self._context_token = None
23
+
24
+ @property
25
+ def id(self) -> str:
26
+ """Returns the ID of this Session."""
27
+ return self._id
28
+
29
+ @property
30
+ def store(self) -> SessionStore:
31
+ """Returns the SessionStore used by this Session."""
32
+ return self._store
33
+
34
+ async def __aenter__(self) -> "Session":
35
+ self._context = Context(
36
+ session_id=self._id,
37
+ store=self._store,
38
+ sequencer=Sequencer(),
39
+ hasher=self._hasher,
40
+ transaction_scope_factory=lambda: TransactionScope(self._store),
41
+ )
42
+ self._context_token = set_context(self._context)
43
+ return self
44
+
45
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
46
+ if self._context and self._context_token:
47
+ # The top-level transaction is managed by the TransactionScope
48
+ # obtained via get_transaction_scope(). We just need to ensure
49
+ # its __aexit__ is called if it was created.
50
+ if top_level_scope := self._context.get_transaction_scope():
51
+ await top_level_scope.__aexit__(exc_type, exc_val, exc_tb)
52
+ reset_context(self._context_token)
@@ -0,0 +1,7 @@
1
+ from .memory import MemorySessionStore
2
+ from .memory_client import MemoryClient
3
+
4
+ __all__ = [
5
+ "MemorySessionStore",
6
+ "MemoryClient",
7
+ ]
glyff/stores/memory.py ADDED
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from ..interfaces import Execution, Serializer, SessionStore, Transaction
7
+ from ..models import ExecutionId, ExecutionRecord, ExecutionStatus
8
+ from .memory_client import MemoryClient
9
+
10
+
11
+ class _MemoryTransaction(Transaction):
12
+ def __init__(self, client: MemoryClient):
13
+ self._client = client
14
+
15
+ async def commit(self) -> None:
16
+ await self._client.commit_staged()
17
+
18
+ async def rollback(self) -> None:
19
+ self._client.clear_staged()
20
+
21
+
22
+ class _MemoryExecution(Execution):
23
+ def __init__(self, store: MemorySessionStore, execution_id: ExecutionId):
24
+ self._store = store
25
+ self._id = execution_id
26
+
27
+ def _id_to_key(self, id: ExecutionId, part: str) -> str:
28
+ return f"execution::{id.name}#{id.sequence}:{id.args_hash}::{part}"
29
+
30
+ async def complete(self, value: Any, return_type: type) -> None:
31
+ self._store._client.stage_write(
32
+ self._id_to_key(self._id, "status"), ExecutionStatus.COMPLETED
33
+ )
34
+ self._store._client.stage_write(
35
+ self._id_to_key(self._id, "result"),
36
+ self._store._serializer.serialize(value, return_type),
37
+ )
38
+
39
+ async def fail(self, error: str) -> None:
40
+ self._store._client.stage_write(
41
+ self._id_to_key(self._id, "status"), ExecutionStatus.FAILED
42
+ )
43
+ self._store._client.stage_write(self._id_to_key(self._id, "error"), error)
44
+
45
+
46
+ class MemorySessionStore(SessionStore):
47
+ """
48
+ An in-memory implementation of SessionStore for testing and development.
49
+ This implementation is not persistent across processes.
50
+ It serializes values to ensure independence, mimicking persisted stores.
51
+ """
52
+
53
+ def __init__(self, client: MemoryClient, serializer: Serializer, **_):
54
+ self._client = client
55
+ self._serializer = serializer
56
+ self._lock = asyncio.Lock()
57
+
58
+ def _id_to_key(self, id: ExecutionId, part: str) -> str:
59
+ return f"execution::{id.name}#{id.sequence}:{id.args_hash}::{part}"
60
+
61
+ async def begin_transaction(self) -> Transaction:
62
+ return _MemoryTransaction(self._client)
63
+
64
+ async def start_execution(self, execution_id: ExecutionId) -> Execution:
65
+ status_key = self._id_to_key(execution_id, "status")
66
+ if await self._client.read(status_key) is None:
67
+ self._client.stage_write(status_key, ExecutionStatus.STARTED)
68
+ return _MemoryExecution(self, execution_id)
69
+
70
+ async def get_execution_record(
71
+ self, execution_id: ExecutionId, return_type: type
72
+ ) -> ExecutionRecord | None:
73
+ status: ExecutionStatus | None = await self._client.read(
74
+ self._id_to_key(execution_id, "status")
75
+ )
76
+ if not status:
77
+ return None
78
+
79
+ result = None
80
+ error = None
81
+
82
+ if status == ExecutionStatus.COMPLETED:
83
+ serialized_value = await self._client.read(
84
+ self._id_to_key(execution_id, "result")
85
+ )
86
+ if serialized_value:
87
+ result = self._serializer.deserialize(serialized_value, return_type)
88
+ elif status == ExecutionStatus.FAILED:
89
+ error = await self._client.read(self._id_to_key(execution_id, "error"))
90
+
91
+ return ExecutionRecord(status=status, result=result, error=error)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+
7
+ class MemoryClient:
8
+ """A low-level in-memory data store with transactional capabilities."""
9
+
10
+ def __init__(self):
11
+ self._data: dict[str, Any] = {}
12
+ self._staged_writes: dict[str, Any] = {}
13
+ self._staged_deletes: set[str] = set()
14
+ self._lock = asyncio.Lock()
15
+
16
+ @property
17
+ def data(self) -> dict[str, Any]:
18
+ return self._data
19
+
20
+ def clear_staged(self) -> None:
21
+ self._staged_writes.clear()
22
+ self._staged_deletes.clear()
23
+
24
+ async def commit_staged(self) -> None:
25
+ async with self._lock:
26
+ self._data.update(self._staged_writes)
27
+ for key in self._staged_deletes:
28
+ self._data.pop(key, None)
29
+ self.clear_staged()
30
+
31
+ async def read(self, key: str) -> Any | None:
32
+ async with self._lock:
33
+ return self._data.get(key)
34
+
35
+ def stage_write(self, key: str, value: Any) -> None:
36
+ self._staged_writes[key] = value
37
+ self._staged_deletes.discard(key)
38
+
39
+ def stage_delete(self, key: str) -> None:
40
+ self._staged_deletes.add(key)
41
+ self._staged_writes.pop(key, None)
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: glyff
3
+ Version: 0.1.0
4
+ Summary: Guaranteed Lightweight Yieldable Function Foundation for checkpointed and resumable task execution.
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 nueruyu
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+ License-File: LICENSE
27
+ Classifier: Development Status :: 3 - Alpha
28
+ Classifier: Framework :: AsyncIO
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Classifier: Operating System :: OS Independent
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Programming Language :: Python :: 3.11
33
+ Classifier: Programming Language :: Python :: 3.12
34
+ Classifier: Programming Language :: Python :: 3.13
35
+ Classifier: Typing :: Typed
36
+ Requires-Python: >=3.11
37
+ Description-Content-Type: text/markdown
38
+
39
+ # glyff
40
+
41
+ **G**uaranteed **L**ightweight **Y**ieldable **F**unction **F**oundation.
42
+
43
+ A primitive for pausing async functions across process and request boundaries,
44
+ and resuming them later from the same point.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install glyff
50
+ ```
51
+
52
+ `glyff` has no dependencies beyond the Python standard library.
53
+
54
+ ## Behavior
55
+
56
+ - Marked function calls are recorded in a session-scoped store, keyed by
57
+ function identity, arguments, and call position.
58
+ - Re-invoking the same call within the same session returns the recorded
59
+ result instead of re-executing.
60
+ - A call's outcome — success or failure — is permanent once recorded.
61
+ - `YieldException` suspends execution at a function boundary; the session
62
+ can be resumed later by entering it again with the same session id.
63
+
64
+ ## Public API
65
+
66
+ | Name | Description |
67
+ | ----------------- | --------------------------------------------------------------- |
68
+ | `engrave` | Decorator that marks an async function for recording. |
69
+ | `Session` | Async context manager that scopes a sequence of engraved calls. |
70
+ | `ExecutionId` | Identifier for a recorded function execution. |
71
+ | `ExecutionRecord` | Persisted execution state and result. |
72
+ | `ExecutionStatus` | Enum: `STARTED`, `COMPLETED`, `FAILED`. |
73
+ | `SessionStore` | Protocol for storage backends. |
74
+ | `Serializer` | Protocol for value serialization. |
75
+ | `ArgsHasher` | Protocol for argument hashing. |
76
+ | `YieldException` | Raised to suspend a session. |
77
+
78
+ ## Extending
79
+
80
+ - For persistent storage, see [`glyff-file-store`](https://pypi.org/project/glyff-file-store/).
81
+ - For Pydantic-typed serialization, see [`glyff-pydantic`](https://pypi.org/project/glyff-pydantic/).
82
+ - Custom backends can be written by implementing the `SessionStore`,
83
+ `Serializer`, and `ArgsHasher` protocols.
84
+
85
+ ## Status
86
+
87
+ Early development. APIs may change before v1.0.
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,18 @@
1
+ glyff/__init__.py,sha256=Pe34jmNPe-42wd4F28DlViR1eG3egfmymKUolzmHBiA,354
2
+ glyff/context.py,sha256=zQafkvNDArFzViYNZEOK43VhcdpotfUj5a5qOCx18ww,5160
3
+ glyff/decorators.py,sha256=eOmSKdcmzCwGZ0homEylYXiGmDS1XCsLq5I9gMMFKhc,1609
4
+ glyff/exceptions.py,sha256=a60joZIVHrgaE9Sa9q8bPAzvBfyBOyY3Za2R2CNSYEM,403
5
+ glyff/executor.py,sha256=d2377qaFZlJJgMgezEPWCfcVsb_Jpig8AHOvmikV1gc,2021
6
+ glyff/interfaces.py,sha256=9c7oVYWyc7emVEWkRT7WNoqlFK334apKB6eVAQ7cgtU,2434
7
+ glyff/models.py,sha256=LQxkbYHpzuJtb90SFJ-gknauVMIHo2H4ieVg1L_LiXo,1219
8
+ glyff/sequencer.py,sha256=vruVm6XcAet3oVuI8NubekZ-snRxqW9pZ_3YKIsOc8w,1404
9
+ glyff/session.py,sha256=65i6DRRzGqa-1G-c7kWf4vDgYI_QAvoUTXg8T6I3bMo,1719
10
+ glyff/serialization/__init__.py,sha256=Bm1my_ChfxgzqWvBWWCuAZ9HVQVZk8Lu86dQYGdC3YQ,108
11
+ glyff/serialization/json.py,sha256=HPledSVYhXY9CJp8bWYLgDqDulflwVcBeXemYA34cKI,1585
12
+ glyff/stores/__init__.py,sha256=EMt0UPcq21XfA_f4hC2_ZeTZGmwvGxWWYPk-P5qrBtI,140
13
+ glyff/stores/memory.py,sha256=BPwlCL8IYeudd_Fcy9GTLIk29bYxYChuvjoQ8QEpa3c,3318
14
+ glyff/stores/memory_client.py,sha256=l-80FXLOaagZ3Rya4ePBA60N2topplx3c0xJbk4OYUs,1189
15
+ glyff-0.1.0.dist-info/METADATA,sha256=6HhB9vH6WNp6fpq53TZCZ2CnBxlhK-TIwDMsA_Og8II,4048
16
+ glyff-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
17
+ glyff-0.1.0.dist-info/licenses/LICENSE,sha256=bNtLcBZk03HGyB7qD0gK37uO4E0sre_G01X1vCFBPG8,1064
18
+ glyff-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nueruyu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.