harness-sdk-python 0.25.1__tar.gz → 0.25.2__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.
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/PKG-INFO +1 -1
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/pyproject.toml +1 -1
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/src/harness_sdk/checkpoints.py +74 -52
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_checkpoints.py +22 -7
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/.gitignore +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/README.md +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/examples/__init__.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/examples/runs_app.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/src/harness_sdk/__init__.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/src/harness_sdk/fenced_postgres.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/src/harness_sdk/linear_thread.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/src/harness_sdk/run_manager.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/run_helpers.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_admit_steer.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_applied_visibility.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_batches.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_branch_anchor.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_dispatching.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_durability.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_edit_dispatched.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_edit_reload.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_enqueue.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_fenced_postgres.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_input_required.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_linear_thread.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_meta.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_outcomes.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_placement.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_prepare_hooks.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_restore.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_rewind_during_run.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_run_crash_sweep.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_settle.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_steer.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_stop_continue.py +0 -0
- {harness_sdk_python-0.25.1 → harness_sdk_python-0.25.2}/tests/test_stopping_window.py +0 -0
|
@@ -11,6 +11,7 @@ Rows and entries are insert-only; ``last_checkpoint_id`` is the only update.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
14
|
+
from contextlib import asynccontextmanager
|
|
14
15
|
from dataclasses import dataclass, field
|
|
15
16
|
from typing import Any, Callable, Protocol
|
|
16
17
|
|
|
@@ -158,50 +159,69 @@ class MemoryCheckpointIndex:
|
|
|
158
159
|
|
|
159
160
|
|
|
160
161
|
class PostgresCheckpointIndex:
|
|
161
|
-
"""psycopg-backed CheckpointIndex
|
|
162
|
+
"""psycopg-backed CheckpointIndex over an ``AsyncConnection`` or ``AsyncConnectionPool``; ``setup()`` creates the two tables."""
|
|
162
163
|
|
|
163
164
|
def __init__(self, conn: Any) -> None:
|
|
164
165
|
try:
|
|
165
|
-
import psycopg
|
|
166
|
+
import psycopg
|
|
167
|
+
import psycopg_pool
|
|
166
168
|
except ImportError as exc:
|
|
167
169
|
raise ImportError(
|
|
168
170
|
"harness_sdk.checkpoints.PostgresCheckpointIndex requires the "
|
|
169
171
|
"'postgres' extra: pip install 'harness-sdk-python[postgres]'"
|
|
170
172
|
) from exc
|
|
173
|
+
if not isinstance(
|
|
174
|
+
conn, (psycopg.AsyncConnection, psycopg_pool.AsyncConnectionPool)
|
|
175
|
+
):
|
|
176
|
+
raise TypeError(
|
|
177
|
+
f"conn must be an AsyncConnection or AsyncConnectionPool, got {conn!r}"
|
|
178
|
+
)
|
|
171
179
|
self._conn = conn
|
|
172
180
|
|
|
181
|
+
@asynccontextmanager
|
|
182
|
+
async def _connection(self):
|
|
183
|
+
import psycopg
|
|
184
|
+
|
|
185
|
+
if isinstance(self._conn, psycopg.AsyncConnection):
|
|
186
|
+
yield self._conn
|
|
187
|
+
else:
|
|
188
|
+
async with self._conn.connection() as conn:
|
|
189
|
+
yield conn
|
|
190
|
+
|
|
173
191
|
async def setup(self) -> None:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
192
|
+
async with self._connection() as conn:
|
|
193
|
+
await conn.execute(
|
|
194
|
+
"CREATE TABLE IF NOT EXISTS checkpoint_threads ("
|
|
195
|
+
" thread_id TEXT NOT NULL,"
|
|
196
|
+
" checkpoint_ns TEXT NOT NULL,"
|
|
197
|
+
" branches JSONB NOT NULL,"
|
|
198
|
+
" PRIMARY KEY (thread_id, checkpoint_ns))"
|
|
199
|
+
)
|
|
200
|
+
await conn.execute(
|
|
201
|
+
"CREATE TABLE IF NOT EXISTS checkpoint_messages ("
|
|
202
|
+
" thread_id TEXT NOT NULL,"
|
|
203
|
+
" checkpoint_ns TEXT NOT NULL,"
|
|
204
|
+
" message_id TEXT NOT NULL,"
|
|
205
|
+
" anchor_checkpoint_id TEXT,"
|
|
206
|
+
" branch_id INTEGER NOT NULL,"
|
|
207
|
+
" PRIMARY KEY (thread_id, checkpoint_ns, message_id))"
|
|
208
|
+
)
|
|
190
209
|
|
|
191
210
|
async def read_thread(
|
|
192
211
|
self, thread_id: str, checkpoint_ns: str
|
|
193
212
|
) -> list[dict[str, Any]]:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
213
|
+
async with self._connection() as conn:
|
|
214
|
+
cur = await conn.execute(
|
|
215
|
+
"SELECT branches FROM checkpoint_threads WHERE thread_id = %s AND checkpoint_ns = %s",
|
|
216
|
+
(thread_id, checkpoint_ns),
|
|
217
|
+
)
|
|
218
|
+
row = await cur.fetchone()
|
|
199
219
|
return row[0] if row is not None else []
|
|
200
220
|
|
|
201
|
-
async def _seed_root(self, thread_id: str, checkpoint_ns: str) -> None:
|
|
221
|
+
async def _seed_root(self, conn: Any, thread_id: str, checkpoint_ns: str) -> None:
|
|
202
222
|
from psycopg.types.json import Jsonb
|
|
203
223
|
|
|
204
|
-
await
|
|
224
|
+
await conn.execute(
|
|
205
225
|
"INSERT INTO checkpoint_threads (thread_id, checkpoint_ns, branches)"
|
|
206
226
|
" VALUES (%s, %s, %s) ON CONFLICT DO NOTHING",
|
|
207
227
|
(thread_id, checkpoint_ns, Jsonb([_ROOT_ENTRY])),
|
|
@@ -223,9 +243,9 @@ class PostgresCheckpointIndex:
|
|
|
223
243
|
"fork_branch": fork_branch,
|
|
224
244
|
"last_checkpoint_id": last_checkpoint_id,
|
|
225
245
|
}
|
|
226
|
-
async with self.
|
|
227
|
-
await self._seed_root(thread_id, checkpoint_ns)
|
|
228
|
-
cur = await
|
|
246
|
+
async with self._connection() as conn, conn.transaction():
|
|
247
|
+
await self._seed_root(conn, thread_id, checkpoint_ns)
|
|
248
|
+
cur = await conn.execute(
|
|
229
249
|
"UPDATE checkpoint_threads SET branches = branches || %s"
|
|
230
250
|
" WHERE thread_id = %s AND checkpoint_ns = %s"
|
|
231
251
|
" RETURNING jsonb_array_length(branches)",
|
|
@@ -240,26 +260,27 @@ class PostgresCheckpointIndex:
|
|
|
240
260
|
branch_ids: list[int],
|
|
241
261
|
checkpoint_id: str,
|
|
242
262
|
) -> None:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
async with self._connection() as conn:
|
|
264
|
+
await self._seed_root(conn, thread_id, checkpoint_ns)
|
|
265
|
+
cur = await conn.execute(
|
|
266
|
+
"SELECT jsonb_array_length(branches) FROM checkpoint_threads"
|
|
267
|
+
" WHERE thread_id = %s AND checkpoint_ns = %s",
|
|
268
|
+
(thread_id, checkpoint_ns),
|
|
269
|
+
)
|
|
270
|
+
row = await cur.fetchone()
|
|
271
|
+
_check_branch_ids(branch_ids, row[0] if row is not None else 0)
|
|
272
|
+
await conn.execute(
|
|
273
|
+
"UPDATE checkpoint_threads SET branches = (SELECT jsonb_agg("
|
|
274
|
+
" CASE WHEN idx::int - 1 = ANY(%s)"
|
|
275
|
+
" THEN elem || jsonb_build_object('last_checkpoint_id', %s::text)"
|
|
276
|
+
" ELSE elem END ORDER BY idx)"
|
|
277
|
+
" FROM jsonb_array_elements(branches) WITH ORDINALITY AS t(elem, idx))"
|
|
278
|
+
" WHERE thread_id = %s AND checkpoint_ns = %s",
|
|
279
|
+
(branch_ids, checkpoint_id, thread_id, checkpoint_ns),
|
|
280
|
+
)
|
|
260
281
|
|
|
261
282
|
async def upsert_messages(self, rows: list[dict[str, Any]]) -> None:
|
|
262
|
-
async with self.
|
|
283
|
+
async with self._connection() as conn, conn.cursor() as cur:
|
|
263
284
|
await cur.executemany(
|
|
264
285
|
"INSERT INTO checkpoint_messages"
|
|
265
286
|
" (thread_id, checkpoint_ns, message_id, anchor_checkpoint_id, branch_id)"
|
|
@@ -272,12 +293,13 @@ class PostgresCheckpointIndex:
|
|
|
272
293
|
async def get_message(
|
|
273
294
|
self, thread_id: str, checkpoint_ns: str, message_id: str
|
|
274
295
|
) -> dict[str, Any] | None:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
296
|
+
async with self._connection() as conn:
|
|
297
|
+
cur = await conn.execute(
|
|
298
|
+
"SELECT anchor_checkpoint_id, branch_id FROM checkpoint_messages"
|
|
299
|
+
" WHERE thread_id = %s AND checkpoint_ns = %s AND message_id = %s",
|
|
300
|
+
(thread_id, checkpoint_ns, message_id),
|
|
301
|
+
)
|
|
302
|
+
row = await cur.fetchone()
|
|
281
303
|
if row is None:
|
|
282
304
|
return None
|
|
283
305
|
return {"anchor_checkpoint_id": row[0], "branch_id": row[1]}
|
|
@@ -663,11 +663,12 @@ def postgres_dsn():
|
|
|
663
663
|
subprocess.run([docker, "rm", "-f", container], capture_output=True)
|
|
664
664
|
|
|
665
665
|
|
|
666
|
-
@pytest.fixture
|
|
667
|
-
async def pg_index(postgres_dsn):
|
|
666
|
+
@pytest.fixture(params=["conn", "pool"])
|
|
667
|
+
async def pg_index(request, postgres_dsn):
|
|
668
668
|
import uuid
|
|
669
669
|
|
|
670
670
|
import psycopg
|
|
671
|
+
from psycopg_pool import AsyncConnectionPool
|
|
671
672
|
|
|
672
673
|
from harness_sdk import PostgresCheckpointIndex
|
|
673
674
|
|
|
@@ -675,15 +676,29 @@ async def pg_index(postgres_dsn):
|
|
|
675
676
|
with psycopg.connect(postgres_dsn, autocommit=True) as admin:
|
|
676
677
|
admin.execute(f'CREATE DATABASE "{dbname}"')
|
|
677
678
|
dsn = postgres_dsn.rsplit("/", 1)[0] + f"/{dbname}"
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
679
|
+
if request.param == "conn":
|
|
680
|
+
conn = await psycopg.AsyncConnection.connect(dsn, autocommit=True)
|
|
681
|
+
index = PostgresCheckpointIndex(conn)
|
|
682
|
+
await index.setup()
|
|
683
|
+
yield index
|
|
684
|
+
await conn.close()
|
|
685
|
+
else:
|
|
686
|
+
async with AsyncConnectionPool(dsn, open=False) as pool:
|
|
687
|
+
index = PostgresCheckpointIndex(pool)
|
|
688
|
+
await index.setup()
|
|
689
|
+
yield index
|
|
683
690
|
with psycopg.connect(postgres_dsn, autocommit=True) as admin:
|
|
684
691
|
admin.execute(f'DROP DATABASE "{dbname}" WITH (FORCE)')
|
|
685
692
|
|
|
686
693
|
|
|
694
|
+
def test_postgres_index_rejects_other_conn_types():
|
|
695
|
+
pytest.importorskip("psycopg")
|
|
696
|
+
from harness_sdk import PostgresCheckpointIndex
|
|
697
|
+
|
|
698
|
+
with pytest.raises(TypeError, match="AsyncConnection or AsyncConnectionPool"):
|
|
699
|
+
PostgresCheckpointIndex(object())
|
|
700
|
+
|
|
701
|
+
|
|
687
702
|
async def test_postgres_branches_round_trip(pg_index):
|
|
688
703
|
assert await pg_index.read_thread(THREAD, "") == []
|
|
689
704
|
first = await pg_index.append_branch(
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|