statepact 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.
Files changed (50) hide show
  1. statepact/__about__.py +3 -0
  2. statepact/__init__.py +157 -0
  3. statepact/cli.py +82 -0
  4. statepact/concurrency/__init__.py +22 -0
  5. statepact/concurrency/cas.py +86 -0
  6. statepact/concurrency/conflict.py +121 -0
  7. statepact/concurrency/locking.py +314 -0
  8. statepact/concurrency/mvcc.py +98 -0
  9. statepact/concurrency/occ.py +66 -0
  10. statepact/concurrency/serializability.py +157 -0
  11. statepact/coordination/__init__.py +18 -0
  12. statepact/coordination/deadlock.py +80 -0
  13. statepact/coordination/fencing.py +60 -0
  14. statepact/coordination/lease.py +101 -0
  15. statepact/coordination/retry.py +132 -0
  16. statepact/coordination/wait_for_graph.py +85 -0
  17. statepact/core/__init__.py +83 -0
  18. statepact/core/context.py +51 -0
  19. statepact/core/exceptions.py +290 -0
  20. statepact/core/idempotency.py +254 -0
  21. statepact/core/interfaces.py +282 -0
  22. statepact/core/manager.py +274 -0
  23. statepact/core/resource.py +130 -0
  24. statepact/core/serde.py +47 -0
  25. statepact/core/session.py +584 -0
  26. statepact/core/transaction.py +231 -0
  27. statepact/core/version.py +62 -0
  28. statepact/integrations/__init__.py +21 -0
  29. statepact/integrations/autogen.py +123 -0
  30. statepact/integrations/base.py +155 -0
  31. statepact/integrations/crewai.py +139 -0
  32. statepact/integrations/langgraph.py +115 -0
  33. statepact/observability/__init__.py +20 -0
  34. statepact/observability/events.py +128 -0
  35. statepact/observability/metrics.py +159 -0
  36. statepact/observability/otel.py +188 -0
  37. statepact/observability/transaction_logs.py +173 -0
  38. statepact/py.typed +0 -0
  39. statepact/storage/__init__.py +5 -0
  40. statepact/storage/base.py +28 -0
  41. statepact/storage/memory.py +637 -0
  42. statepact/storage/postgres.py +906 -0
  43. statepact/storage/redis.py +837 -0
  44. statepact/testing/__init__.py +17 -0
  45. statepact/testing/concurrent_runner.py +110 -0
  46. statepact-0.1.0.dist-info/METADATA +447 -0
  47. statepact-0.1.0.dist-info/RECORD +50 -0
  48. statepact-0.1.0.dist-info/WHEEL +4 -0
  49. statepact-0.1.0.dist-info/entry_points.txt +2 -0
  50. statepact-0.1.0.dist-info/licenses/LICENSE +201 -0
statepact/__about__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Single source of truth for the distribution version."""
2
+
3
+ __version__ = "0.1.0"
statepact/__init__.py ADDED
@@ -0,0 +1,157 @@
1
+ """StatePact: transaction management for autonomous AI agents."""
2
+
3
+ from statepact.__about__ import __version__
4
+ from statepact.concurrency.cas import compare_and_delete, compare_and_swap
5
+ from statepact.concurrency.conflict import Conflict
6
+ from statepact.concurrency.locking import LockManager
7
+ from statepact.concurrency.serializability import SerializationGraphAudit
8
+ from statepact.coordination.fencing import FenceGuard
9
+ from statepact.coordination.lease import LeaseManager
10
+ from statepact.coordination.retry import RetryOutcome, RetryPolicy, run_with_retry
11
+ from statepact.core.context import current_session, require_session
12
+ from statepact.core.exceptions import (
13
+ AmbiguousResourceError,
14
+ BackendNotSupportedError,
15
+ BackendUnavailableError,
16
+ ConfigurationError,
17
+ ConflictError,
18
+ DeadlockError,
19
+ EffectFailedError,
20
+ EffectInProgressError,
21
+ IdempotencyError,
22
+ IdempotencyMismatchError,
23
+ KeyNotFoundError,
24
+ LeaseLostError,
25
+ LeaseNotAvailableError,
26
+ LockTimeoutError,
27
+ ReadWriteConflict,
28
+ ResourceError,
29
+ ResourceExistsError,
30
+ ResourceNotFoundError,
31
+ ResourceNotInSessionError,
32
+ RetryExhaustedError,
33
+ SerializationError,
34
+ SideEffectError,
35
+ StaleSnapshotError,
36
+ StatePactError,
37
+ StorageError,
38
+ TransactionAbortedError,
39
+ TransactionError,
40
+ TransactionStateError,
41
+ ValidationError,
42
+ WriteWriteConflict,
43
+ )
44
+ from statepact.core.idempotency import (
45
+ EffectRecord,
46
+ EffectState,
47
+ EffectStore,
48
+ IdempotentExecutor,
49
+ MemoryEffectStore,
50
+ run_idempotent,
51
+ )
52
+ from statepact.core.interfaces import (
53
+ ISOLATION_MODES,
54
+ CommitPlan,
55
+ CommitResult,
56
+ IsolationMode,
57
+ Lease,
58
+ ReadRecord,
59
+ ResourceSnapshot,
60
+ StorageBackend,
61
+ StoredValue,
62
+ WriteRecord,
63
+ )
64
+ from statepact.core.manager import (
65
+ TransactionManager,
66
+ default_manager,
67
+ reset_default_manager,
68
+ set_default_manager,
69
+ )
70
+ from statepact.core.resource import SharedResource
71
+ from statepact.core.session import AgentSession
72
+ from statepact.core.transaction import TransactionState
73
+ from statepact.observability.events import Event, EventBus, EventType
74
+ from statepact.observability.metrics import Metrics
75
+ from statepact.observability.transaction_logs import (
76
+ TransactionLog,
77
+ TransactionRecord,
78
+ TransactionStatus,
79
+ )
80
+ from statepact.storage.memory import MemoryBackend
81
+
82
+ __all__ = [
83
+ "ISOLATION_MODES",
84
+ "AgentSession",
85
+ "AmbiguousResourceError",
86
+ "BackendNotSupportedError",
87
+ "BackendUnavailableError",
88
+ "CommitPlan",
89
+ "CommitResult",
90
+ "ConfigurationError",
91
+ "Conflict",
92
+ "ConflictError",
93
+ "DeadlockError",
94
+ "EffectFailedError",
95
+ "EffectInProgressError",
96
+ "EffectRecord",
97
+ "EffectState",
98
+ "EffectStore",
99
+ "Event",
100
+ "EventBus",
101
+ "EventType",
102
+ "FenceGuard",
103
+ "IdempotencyError",
104
+ "IdempotencyMismatchError",
105
+ "IdempotentExecutor",
106
+ "IsolationMode",
107
+ "KeyNotFoundError",
108
+ "Lease",
109
+ "LeaseLostError",
110
+ "LeaseManager",
111
+ "LeaseNotAvailableError",
112
+ "LockManager",
113
+ "LockTimeoutError",
114
+ "MemoryBackend",
115
+ "MemoryEffectStore",
116
+ "Metrics",
117
+ "ReadRecord",
118
+ "ReadWriteConflict",
119
+ "ResourceError",
120
+ "ResourceExistsError",
121
+ "ResourceNotFoundError",
122
+ "ResourceNotInSessionError",
123
+ "ResourceSnapshot",
124
+ "RetryExhaustedError",
125
+ "RetryOutcome",
126
+ "RetryPolicy",
127
+ "SerializationError",
128
+ "SerializationGraphAudit",
129
+ "SharedResource",
130
+ "SideEffectError",
131
+ "StaleSnapshotError",
132
+ "StatePactError",
133
+ "StorageBackend",
134
+ "StorageError",
135
+ "StoredValue",
136
+ "TransactionAbortedError",
137
+ "TransactionError",
138
+ "TransactionLog",
139
+ "TransactionManager",
140
+ "TransactionRecord",
141
+ "TransactionState",
142
+ "TransactionStateError",
143
+ "TransactionStatus",
144
+ "ValidationError",
145
+ "WriteRecord",
146
+ "WriteWriteConflict",
147
+ "__version__",
148
+ "compare_and_delete",
149
+ "compare_and_swap",
150
+ "current_session",
151
+ "default_manager",
152
+ "require_session",
153
+ "reset_default_manager",
154
+ "run_idempotent",
155
+ "run_with_retry",
156
+ "set_default_manager",
157
+ ]
statepact/cli.py ADDED
@@ -0,0 +1,82 @@
1
+ """Command line entry point: ``statepact``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import json
8
+ import platform
9
+ import sys
10
+ from collections.abc import Sequence
11
+
12
+ from statepact.__about__ import __version__
13
+
14
+
15
+ def _info() -> int:
16
+ from statepact.observability.events import EventBus
17
+ from statepact.storage.memory import MemoryBackend
18
+
19
+ backend = MemoryBackend(event_bus=EventBus())
20
+ payload = {
21
+ "statepact": __version__,
22
+ "python": sys.version.split()[0],
23
+ "platform": platform.platform(),
24
+ "backend": backend.name,
25
+ "capabilities": {
26
+ "mvcc": backend.supports_mvcc,
27
+ "locking": backend.supports_locking,
28
+ "leases": backend.supports_leases,
29
+ "idempotency": backend.supports_idempotency,
30
+ "durable": backend.durable,
31
+ },
32
+ }
33
+ print(json.dumps(payload, indent=2))
34
+ return 0
35
+
36
+
37
+ async def _smoke() -> int:
38
+ from statepact import TransactionManager
39
+
40
+ manager = TransactionManager()
41
+ inventory = manager.resource("inventory", {"sku": 1})
42
+ async with manager.session(agent_id="smoke", resources=[inventory]) as session:
43
+ stock = await session.read("sku")
44
+ await session.write("sku", stock - 1)
45
+ result = await session.commit()
46
+ remaining = await inventory.read("sku")
47
+ ok = remaining == 0 and result.committed
48
+ print(
49
+ json.dumps(
50
+ {
51
+ "ok": ok,
52
+ "remaining": remaining,
53
+ "commit_version": result.commit_version,
54
+ "transaction_id": result.transaction_id,
55
+ },
56
+ indent=2,
57
+ )
58
+ )
59
+ await manager.close()
60
+ return 0 if ok else 1
61
+
62
+
63
+ def main(argv: Sequence[str] | None = None) -> int:
64
+ parser = argparse.ArgumentParser(
65
+ prog="statepact", description="Transaction management for autonomous AI agents"
66
+ )
67
+ parser.add_argument("--version", action="version", version=f"statepact {__version__}")
68
+ subparsers = parser.add_subparsers(dest="command")
69
+ subparsers.add_parser("info", help="print version, platform and backend capabilities")
70
+ subparsers.add_parser("smoke", help="run a one-transaction installation smoke test")
71
+ args = parser.parse_args(argv)
72
+
73
+ if args.command == "info":
74
+ return _info()
75
+ if args.command == "smoke":
76
+ return asyncio.run(_smoke())
77
+ parser.print_help()
78
+ return 0
79
+
80
+
81
+ if __name__ == "__main__": # pragma: no cover
82
+ raise SystemExit(main())
@@ -0,0 +1,22 @@
1
+ """Concurrency control algorithms: OCC, MVCC, 2PL, conflict detection, CAS."""
2
+
3
+ from statepact.concurrency.cas import compare_and_swap
4
+ from statepact.concurrency.conflict import Conflict, detect
5
+ from statepact.concurrency.locking import LockManager
6
+ from statepact.concurrency.mvcc import VersionEntry, snapshot_values, visible_entry
7
+ from statepact.concurrency.serializability import (
8
+ SerializationGraphAudit,
9
+ TransactionFootprint,
10
+ )
11
+
12
+ __all__ = [
13
+ "Conflict",
14
+ "LockManager",
15
+ "SerializationGraphAudit",
16
+ "TransactionFootprint",
17
+ "VersionEntry",
18
+ "compare_and_swap",
19
+ "detect",
20
+ "snapshot_values",
21
+ "visible_entry",
22
+ ]
@@ -0,0 +1,86 @@
1
+ """Compare-and-swap helper built on the transaction engine.
2
+
3
+ CAS is the smallest useful primitive for agents: "set this key to a new value only
4
+ if it is still at the version I observed". It is implemented as a one-operation
5
+ transaction so it inherits every backend's atomic validation; it is not a separate
6
+ code path that could silently diverge.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from statepact.core.exceptions import ConflictError
14
+ from statepact.core.interfaces import StorageBackend
15
+ from statepact.core.serde import normalize_value
16
+ from statepact.core.transaction import Transaction, new_transaction_id, utcnow
17
+ from statepact.core.version import UNVERSIONED, Version
18
+
19
+
20
+ async def compare_and_swap(
21
+ backend: StorageBackend,
22
+ resource: str,
23
+ key: str,
24
+ *,
25
+ expected_version: Version,
26
+ value: Any,
27
+ agent_id: str = "cas",
28
+ ) -> bool:
29
+ """Atomically write ``value`` iff ``key`` is at ``expected_version``.
30
+
31
+ ``expected_version=0`` means "only if the key does not exist" (insert).
32
+ Returns ``True`` on success and ``False`` on a version conflict. Backend or
33
+ configuration errors are never swallowed.
34
+ """
35
+
36
+ normalized = normalize_value(value)
37
+ transaction = Transaction(
38
+ id=new_transaction_id(),
39
+ agent_id=agent_id,
40
+ isolation="optimistic",
41
+ resources=(resource,),
42
+ started_at=utcnow(),
43
+ fence=None,
44
+ )
45
+ transaction.stage_write(resource, key, normalized, expected_version)
46
+ transaction.record_read(resource, key, expected_version)
47
+ transaction.mark_committing()
48
+ try:
49
+ await backend.commit(transaction.to_plan())
50
+ except ConflictError:
51
+ transaction.mark_aborted("compare-and-swap conflict")
52
+ return False
53
+ return True
54
+
55
+
56
+ async def compare_and_delete(
57
+ backend: StorageBackend,
58
+ resource: str,
59
+ key: str,
60
+ *,
61
+ expected_version: Version,
62
+ agent_id: str = "cas",
63
+ ) -> bool:
64
+ """Delete ``key`` iff it is still at ``expected_version``."""
65
+
66
+ transaction = Transaction(
67
+ id=new_transaction_id(),
68
+ agent_id=agent_id,
69
+ isolation="optimistic",
70
+ resources=(resource,),
71
+ started_at=utcnow(),
72
+ fence=None,
73
+ )
74
+ transaction.stage_write(resource, key, None, expected_version, op="delete")
75
+ transaction.record_read(resource, key, expected_version)
76
+ transaction.mark_committing()
77
+ try:
78
+ await backend.commit(transaction.to_plan())
79
+ except ConflictError:
80
+ transaction.mark_aborted("compare-and-delete conflict")
81
+ return False
82
+ return True
83
+
84
+
85
+ def is_absent(version: Version) -> bool:
86
+ return version == UNVERSIONED
@@ -0,0 +1,121 @@
1
+ """Conflict classification and detection.
2
+
3
+ The detector works on ``(resource, key) -> latest version`` maps supplied by the
4
+ backend *inside* its atomic commit section. Keeping it pure lets the same rules
5
+ be exercised by unit and property tests without a backend.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping, Sequence
11
+ from dataclasses import dataclass
12
+
13
+ from statepact.core.exceptions import (
14
+ ConflictClass,
15
+ ConflictError,
16
+ ReadWriteConflict,
17
+ WriteWriteConflict,
18
+ )
19
+ from statepact.core.interfaces import CommitPlan, ReadRecord, WriteRecord
20
+ from statepact.core.version import Version
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class Conflict:
25
+ """A detected version conflict, before it is turned into an exception."""
26
+
27
+ kind: ConflictClass
28
+ transaction_id: str
29
+ resource: str
30
+ key: str
31
+ expected_version: Version
32
+ actual_version: Version
33
+ description: str
34
+
35
+ def to_exception(self) -> ConflictError:
36
+ kwargs: dict[str, object] = {
37
+ "transaction_id": self.transaction_id,
38
+ "resource": self.resource,
39
+ "key": self.key,
40
+ "expected_version": self.expected_version,
41
+ "actual_version": self.actual_version,
42
+ }
43
+ if self.kind == "read_write":
44
+ return ReadWriteConflict(**kwargs)
45
+ return WriteWriteConflict(**kwargs)
46
+
47
+
48
+ def check_read(record: ReadRecord, latest_version: Version, transaction_id: str) -> Conflict | None:
49
+ if latest_version == record.version:
50
+ return None
51
+ return Conflict(
52
+ kind="read_write",
53
+ transaction_id=transaction_id,
54
+ resource=record.resource,
55
+ key=record.key,
56
+ expected_version=record.version,
57
+ actual_version=latest_version,
58
+ description=(
59
+ f"{record.resource}.{record.key} was read at version {record.version} "
60
+ f"but is now at {latest_version}"
61
+ ),
62
+ )
63
+
64
+
65
+ def check_write(
66
+ record: WriteRecord, latest_version: Version, transaction_id: str
67
+ ) -> Conflict | None:
68
+ if latest_version == record.expected_version:
69
+ return None
70
+ return Conflict(
71
+ kind="write_write",
72
+ transaction_id=transaction_id,
73
+ resource=record.resource,
74
+ key=record.key,
75
+ expected_version=record.expected_version,
76
+ actual_version=latest_version,
77
+ description=(
78
+ f"{record.resource}.{record.key} was staged on version "
79
+ f"{record.expected_version} but is now at {latest_version}"
80
+ ),
81
+ )
82
+
83
+
84
+ def detect(
85
+ plan: CommitPlan,
86
+ latest: Mapping[tuple[str, str], Version],
87
+ *,
88
+ validate_reads: bool,
89
+ validate_writes: bool,
90
+ ) -> Conflict | None:
91
+ """Return the first conflict in a stable (read order, then write order) scan."""
92
+
93
+ if validate_reads:
94
+ for read in plan.reads:
95
+ actual = latest.get((read.resource, read.key), 0)
96
+ conflict = check_read(read, actual, plan.transaction_id)
97
+ if conflict is not None:
98
+ return conflict
99
+ if validate_writes:
100
+ for write in plan.writes:
101
+ actual = latest.get((write.resource, write.key), 0)
102
+ conflict = check_write(write, actual, plan.transaction_id)
103
+ if conflict is not None:
104
+ return conflict
105
+ return None
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class ValidationOutcome:
110
+ ok: bool
111
+ conflict: Conflict | None = None
112
+
113
+ def raise_if_conflict(self) -> None:
114
+ if self.conflict is not None:
115
+ raise self.conflict.to_exception()
116
+
117
+
118
+ def involved_keys(plan: CommitPlan) -> Sequence[tuple[str, str]]:
119
+ keys = {(read.resource, read.key) for read in plan.reads}
120
+ keys.update((write.resource, write.key) for write in plan.writes)
121
+ return tuple(sorted(keys))