odysseys-py 0.0.1__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.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: odysseys-py
3
+ Version: 0.0.1
4
+ Summary: A lightweight durable execution and orchestration runtime for distributed services.
5
+ Author: Sreejay Reddy
6
+ License: MIT
7
+ Keywords: workflow,durable-execution,orchestration,distributed-systems,postgres
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: psycopg[binary]>=3.2
19
+ Requires-Dist: PyYAML>=6.0
20
+ Requires-Dist: python-dotenv>=1.0
File without changes
File without changes
File without changes
@@ -0,0 +1,64 @@
1
+ class Build_ledger:
2
+ def __init__(self, get_conn, key, steps, delegates):
3
+ self.get_conn = get_conn
4
+ self.key = key
5
+ self.steps = steps
6
+ self.delegates = delegates or {}
7
+
8
+ def run(self):
9
+
10
+ if not self.steps:
11
+ raise ValueError("build_ledger requires at least one step")
12
+
13
+ if len(self.steps) != len(set(self.steps)):
14
+ raise ValueError("steps must be unique")
15
+
16
+ unknown = set(self.delegates) - set(self.steps)
17
+ if unknown:
18
+ raise ValueError(f"delegates references steps not present in `steps`: {sorted(unknown)}")
19
+
20
+ conn = self.get_conn()
21
+ ledger_rows = []
22
+ delivery_rows = []
23
+
24
+ try:
25
+ for target in self.steps:
26
+
27
+ if target in self.delegates:
28
+ destination = self.delegates.get(target)
29
+ delivery_rows.append((self.key, target, destination))
30
+ ledger_rows.append((self.key, target, "delegated"))
31
+ else:
32
+ ledger_rows.append((self.key, target, "local"))
33
+
34
+ with conn.cursor() as cur:
35
+ cur.executemany("""
36
+ INSERT INTO odyssey_ledger(
37
+ key,
38
+ target,
39
+ mode
40
+ )
41
+ VALUES(%s,%s,%s)
42
+ """, ledger_rows, )
43
+
44
+ cur.executemany("""
45
+ INSERT INTO odyssey_deliveries(
46
+ key,
47
+ target,
48
+ emit_to
49
+ )
50
+ VALUES(%s, %s, %s)
51
+ """, delivery_rows, )
52
+
53
+ conn.commit()
54
+ return {
55
+ "steps": len(ledger_rows),
56
+ "delegated": len(delivery_rows)
57
+ }
58
+
59
+ except Exception as e:
60
+ conn.rollback()
61
+ raise RuntimeError("Failed to build ledger") from e
62
+
63
+ finally:
64
+ conn.close()
File without changes
@@ -0,0 +1,171 @@
1
+ import json
2
+ from .utils import get_owner_id, row_to_dict
3
+ from .results import AcquireResult, OperationResult, InspectResult
4
+
5
+ def acquire(conn, key, *, owner_id=None, ttl_ms=10000):
6
+
7
+ owner_id = owner_id or get_owner_id()
8
+ ttl_ms = ttl_ms if ttl_ms and ttl_ms > 0 else 10000
9
+
10
+ row = None
11
+
12
+ with conn.cursor() as cur:
13
+ cur.execute("""
14
+ INSERT INTO odyssey_journeys (
15
+ key,
16
+ owner_id,
17
+ expires_at,
18
+ updated_at,
19
+ fencing_token
20
+ )
21
+ VALUES (
22
+ %s,
23
+ %s,
24
+ NOW() + (%s * INTERVAL '1 millisecond'),
25
+ NOW(),
26
+ nextval('odyssey_token_seq')
27
+ )
28
+ ON CONFLICT (key)
29
+ DO UPDATE
30
+ SET
31
+ owner_id = EXCLUDED.owner_id,
32
+ expires_at = EXCLUDED.expires_at,
33
+ updated_at = NOW(),
34
+ fencing_token = nextval('odyssey_token_seq')
35
+ WHERE odyssey_journeys.expires_at < NOW() AND odyssey_journeys.status = 'claimed'
36
+ RETURNING owner_id, expires_at, fencing_token, status, expires_at > NOW() AS journey_alive;
37
+ """, (key, owner_id, ttl_ms))
38
+
39
+ result = cur.fetchone()
40
+
41
+ if result is not None:
42
+ row = row_to_dict(cur, result)
43
+
44
+ conn.commit()
45
+
46
+ if row is not None and row["fencing_token"] is None:
47
+ raise Exception("Invariant violation: fencing_token is None")
48
+
49
+ if row is not None:
50
+ return AcquireResult(
51
+ acquired=True,
52
+ owner_id=row["owner_id"],
53
+ expires_at=row["expires_at"],
54
+ journey_alive=row["journey_alive"],
55
+ fencing_token=row["fencing_token"],
56
+ status=row["status"]
57
+ )
58
+
59
+ with conn.cursor() as cur:
60
+ cur.execute("""
61
+ SELECT owner_id, expires_at, fencing_token, status, expires_at > NOW() AS journey_alive
62
+ FROM odyssey_journeys
63
+ WHERE key = %s
64
+ """, (key,))
65
+
66
+ result = cur.fetchone()
67
+
68
+ if result is not None:
69
+ row = row_to_dict(cur, result)
70
+
71
+ if row is not None:
72
+ return AcquireResult(acquired=False,
73
+ owner_id=row["owner_id"],
74
+ expires_at=row["expires_at"],
75
+ journey_alive=row["journey_alive"],
76
+ fencing_token=row["fencing_token"],
77
+ status=row["status"])
78
+
79
+ def start_execution(conn, key, *, fencing_token):
80
+
81
+ row = None
82
+
83
+ with conn.cursor() as cur:
84
+ cur.execute("""
85
+ UPDATE odyssey_journeys
86
+ SET status = 'executing',
87
+ updated_at = NOW()
88
+ WHERE key = %s
89
+ AND fencing_token = %s
90
+ AND status = 'claimed'
91
+ RETURNING status;
92
+ """, (key, fencing_token))
93
+
94
+ result = cur.fetchone()
95
+ success = result is not None
96
+ if result is not None:
97
+ row = row_to_dict(cur, result)
98
+
99
+ conn.commit()
100
+ if row is None:
101
+ return OperationResult(success)
102
+
103
+ return OperationResult(success, status=row["status"])
104
+
105
+ def complete(conn, key, *, fencing_token, execution_result=None):
106
+
107
+ serialized_result = (
108
+ json.dumps(execution_result)
109
+ if execution_result is not None
110
+ else None
111
+ )
112
+
113
+ with conn.cursor() as cur:
114
+ cur.execute("""
115
+ UPDATE odyssey_journeys
116
+ SET
117
+ status = 'completed',
118
+ execution_result = %s,
119
+ updated_at = NOW()
120
+ WHERE key = %s
121
+ AND fencing_token = %s
122
+ AND status = 'executing'
123
+ RETURNING 1;
124
+ """, (serialized_result, key, fencing_token))
125
+
126
+ success = cur.fetchone() is not None
127
+
128
+ conn.commit()
129
+ return OperationResult(success)
130
+
131
+ def abandon(conn, key, *, fencing_token):
132
+ with conn.cursor() as cur:
133
+ cur.execute("""
134
+ UPDATE odyssey_journeys
135
+ SET expires_at = NOW(),
136
+ updated_at = NOW()
137
+ WHERE key = %s
138
+ AND fencing_token = %s
139
+ AND status = 'executing'
140
+ RETURNING 1;
141
+ """, (key, fencing_token))
142
+ success = cur.fetchone() is not None
143
+
144
+ conn.commit()
145
+ return OperationResult(success)
146
+
147
+ def inspect(conn, key):
148
+ with conn.cursor() as cur:
149
+ cur.execute("""
150
+ SELECT owner_id, expires_at, updated_at, fencing_token, status,
151
+ expires_at > NOW() AS journey_alive, execution_result
152
+ FROM odyssey_journeys
153
+ WHERE key = %s
154
+ """, (key,))
155
+
156
+ result = cur.fetchone()
157
+
158
+ if result is None:
159
+ return None
160
+
161
+ row = row_to_dict(cur, result)
162
+
163
+ return InspectResult(
164
+ key = key,
165
+ owner_id = row["owner_id"],
166
+ fencing_token = row["fencing_token"],
167
+ status = row["status"],
168
+ journey_alive = row["journey_alive"],
169
+ expires_at = row["expires_at"],
170
+ updated_at = row["updated_at"],
171
+ execution_result = row["execution_result"])
File without changes
@@ -0,0 +1,29 @@
1
+ # Response structure of acquire
2
+ class AcquireResult:
3
+ def __init__(self, acquired, owner_id=None, expires_at=None, fencing_token=None, status=None, journey_alive=None):
4
+ self.acquired = acquired
5
+ self.owner_id = owner_id
6
+ self.expires_at = expires_at
7
+ self.fencing_token = fencing_token
8
+ self.status = status
9
+ self.journey_alive = journey_alive
10
+
11
+ class OperationResult:
12
+ def __init__(self, success: bool, status=None):
13
+ self.success = success
14
+ self.status = status
15
+
16
+ class InspectResult:
17
+ def __init__(
18
+ self, key, owner_id, fencing_token, status, journey_alive,
19
+ expires_at, updated_at, execution_result=None
20
+ ):
21
+ self.key = key
22
+ self.owner_id = owner_id
23
+ self.fencing_token = fencing_token
24
+ self.status = status
25
+ self.journey_alive = journey_alive
26
+ self.expires_at = expires_at
27
+ self.updated_at = updated_at
28
+ self.execution_result = execution_result
29
+
@@ -0,0 +1,78 @@
1
+ SCHEMA_SQL = """
2
+ CREATE SEQUENCE IF NOT EXISTS odyssey_token_seq;
3
+
4
+ DO $$
5
+ BEGIN
6
+ CREATE TYPE odyssey_status AS ENUM (
7
+ 'claimed',
8
+ 'executing',
9
+ 'completed',
10
+ 'reconciling'
11
+ );
12
+ EXCEPTION
13
+ WHEN duplicate_object THEN NULL;
14
+ END $$;
15
+
16
+ DO $$
17
+ BEGIN
18
+ CREATE TYPE odyssey_execution_mode AS ENUM (
19
+ 'local',
20
+ 'delegated'
21
+ );
22
+ EXCEPTION
23
+ WHEN duplicate_object THEN NULL;
24
+ END $$;
25
+
26
+ DO $$
27
+ BEGIN
28
+ CREATE TYPE delivery_status AS ENUM (
29
+ 'delivered',
30
+ 'failed',
31
+ 'pending'
32
+ );
33
+ EXCEPTION
34
+ WHEN duplicate_object THEN NULL;
35
+ END $$;
36
+
37
+ CREATE TABLE IF NOT EXISTS odyssey_ledger (
38
+ key TEXT NOT NULL,
39
+ target TEXT NOT NULL,
40
+ status odyssey_status NOT NULL DEFAULT 'claimed',
41
+ mode odyssey_execution_mode NOT NULL,
42
+ started_at TIMESTAMPTZ,
43
+ completed_at TIMESTAMPTZ,
44
+
45
+ PRIMARY KEY (key, target)
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS odyssey_journeys (
49
+ key TEXT NOT NULL,
50
+ target TEXT NOT NULL,
51
+ owner_id TEXT NOT NULL,
52
+ expires_at TIMESTAMPTZ NOT NULL,
53
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
54
+ execution_result JSONB,
55
+ status odyssey_status NOT NULL DEFAULT 'claimed',
56
+ fencing_token BIGINT NOT NULL DEFAULT 1,
57
+
58
+ PRIMARY KEY (key, target),
59
+ FOREIGN KEY (key, target)
60
+ REFERENCES odyssey_ledger(key, target)
61
+ );
62
+
63
+ CREATE TABLE IF NOT EXISTS odyssey_deliveries (
64
+ key TEXT NOT NULL,
65
+ target TEXT NOT NULL,
66
+ emit_to TEXT NOT NULL,
67
+ payload JSONB,
68
+ status delivery_status NOT NULL DEFAULT 'pending',
69
+ attempts INTEGER NOT NULL DEFAULT 0,
70
+ last_attempt_at TIMESTAMPTZ,
71
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
72
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
73
+
74
+ PRIMARY KEY (key, target, emit_to),
75
+ FOREIGN KEY (key, target)
76
+ REFERENCES odyssey_ledger(key, target)
77
+ );
78
+ """
@@ -0,0 +1,14 @@
1
+ import os
2
+ import threading
3
+ import socket
4
+
5
+ def get_owner_id():
6
+ hostname = socket.gethostname()
7
+ pid = os.getpid()
8
+ thread_id = threading.get_ident()
9
+
10
+ return f"{hostname}:{pid}-{thread_id}"
11
+
12
+ def row_to_dict(cursor, row):
13
+ columns = [desc[0] for desc in cursor.description]
14
+ return dict(zip(columns, row))
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: odysseys-py
3
+ Version: 0.0.1
4
+ Summary: A lightweight durable execution and orchestration runtime for distributed services.
5
+ Author: Sreejay Reddy
6
+ License: MIT
7
+ Keywords: workflow,durable-execution,orchestration,distributed-systems,postgres
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: psycopg[binary]>=3.2
19
+ Requires-Dist: PyYAML>=6.0
20
+ Requires-Dist: python-dotenv>=1.0
@@ -0,0 +1,16 @@
1
+ pyproject.toml
2
+ odyssey/__init__.py
3
+ odyssey/async_core.py
4
+ odyssey/async_odyssey.py
5
+ odyssey/build_ledger.py
6
+ odyssey/config.py
7
+ odyssey/core.py
8
+ odyssey/odyssey.py
9
+ odyssey/results.py
10
+ odyssey/schema.py
11
+ odyssey/utils.py
12
+ odysseys_py.egg-info/PKG-INFO
13
+ odysseys_py.egg-info/SOURCES.txt
14
+ odysseys_py.egg-info/dependency_links.txt
15
+ odysseys_py.egg-info/requires.txt
16
+ odysseys_py.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ psycopg[binary]>=3.2
2
+ PyYAML>=6.0
3
+ python-dotenv>=1.0
@@ -0,0 +1 @@
1
+ odyssey
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "odysseys-py"
7
+ version = "0.0.1"
8
+ description = "A lightweight durable execution and orchestration runtime for distributed services."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+
13
+ authors = [
14
+ { name = "Sreejay Reddy" }
15
+ ]
16
+
17
+ dependencies = [
18
+ "psycopg[binary]>=3.2",
19
+ "PyYAML>=6.0",
20
+ "python-dotenv>=1.0",
21
+ ]
22
+
23
+ keywords = [
24
+ "workflow",
25
+ "durable-execution",
26
+ "orchestration",
27
+ "distributed-systems",
28
+ "postgres",
29
+ ]
30
+
31
+ classifiers = [
32
+ "Development Status :: 3 - Alpha",
33
+ "Intended Audience :: Developers",
34
+ "License :: OSI Approved :: MIT License",
35
+ "Programming Language :: Python :: 3",
36
+ "Programming Language :: Python :: 3.10",
37
+ "Programming Language :: Python :: 3.11",
38
+ "Programming Language :: Python :: 3.12",
39
+ "Operating System :: OS Independent",
40
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+