jharness-repository 0.2.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.
- jharness_repository-0.2.2/.gitignore +36 -0
- jharness_repository-0.2.2/LICENSE +21 -0
- jharness_repository-0.2.2/PKG-INFO +53 -0
- jharness_repository-0.2.2/README.md +27 -0
- jharness_repository-0.2.2/pyproject.toml +41 -0
- jharness_repository-0.2.2/src/jharness/repository/__init__.py +13 -0
- jharness_repository-0.2.2/src/jharness/repository/_codec.py +72 -0
- jharness_repository-0.2.2/src/jharness/repository/memory.py +70 -0
- jharness_repository-0.2.2/src/jharness/repository/mysql.py +635 -0
- jharness_repository-0.2.2/src/jharness/repository/py.typed +1 -0
- jharness_repository-0.2.2/src/jharness/repository/redis.py +588 -0
- jharness_repository-0.2.2/src/jharness/repository/sqlite.py +399 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# OS/editor noise
|
|
2
|
+
.DS_Store
|
|
3
|
+
Thumbs.db
|
|
4
|
+
*.swp
|
|
5
|
+
*.swo
|
|
6
|
+
|
|
7
|
+
# Local assistant state
|
|
8
|
+
.codex/
|
|
9
|
+
.claude/
|
|
10
|
+
.gemini/
|
|
11
|
+
.aider*
|
|
12
|
+
|
|
13
|
+
# Python
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
.python-version
|
|
17
|
+
.venv/
|
|
18
|
+
venv/
|
|
19
|
+
ENV/
|
|
20
|
+
|
|
21
|
+
# Python tooling caches and reports
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.pyright/
|
|
25
|
+
.mypy_cache/
|
|
26
|
+
.coverage
|
|
27
|
+
coverage/
|
|
28
|
+
htmlcov/
|
|
29
|
+
|
|
30
|
+
# Packaging/build artifacts
|
|
31
|
+
dist/
|
|
32
|
+
build/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
|
|
35
|
+
# Logs
|
|
36
|
+
*.log
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JHarness contributors
|
|
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.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jharness-repository
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Memory, SQLite, MySQL, and Redis checkpoint repositories for JHarness
|
|
5
|
+
Project-URL: Documentation, https://github.com/Ezio2000/jharness/tree/main/docs
|
|
6
|
+
Project-URL: Repository, https://github.com/Ezio2000/jharness.git
|
|
7
|
+
Author: JHarness contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: jharness-kernel==0.2.2
|
|
21
|
+
Provides-Extra: mysql
|
|
22
|
+
Requires-Dist: pymysql[rsa]>=1.2.0; extra == 'mysql'
|
|
23
|
+
Provides-Extra: redis
|
|
24
|
+
Requires-Dist: redis>=8.0.1; extra == 'redis'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# jharness-repository
|
|
28
|
+
|
|
29
|
+
Official in-memory, SQLite, MySQL, and Redis implementations of the JHarness
|
|
30
|
+
`RunRepository` protocol.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Memory and SQLite only
|
|
34
|
+
uv add jharness-repository
|
|
35
|
+
|
|
36
|
+
# Add exactly the remote drivers the application uses
|
|
37
|
+
uv add "jharness-repository[mysql]"
|
|
38
|
+
uv add "jharness-repository[redis]"
|
|
39
|
+
uv add "jharness-repository[mysql,redis]"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from jharness.repository import MemoryRunRepository
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The base installation has no database-driver dependency. `MemoryRunRepository` needs
|
|
47
|
+
no service, and SQLite uses Python's embedded `sqlite3` module. MySQL and Redis load
|
|
48
|
+
their selected client only when that backend is initialized; there is no fallback or
|
|
49
|
+
compatibility driver. Their services remain supplied by the application, while the
|
|
50
|
+
JHarness development suite exercises them in disposable Docker containers.
|
|
51
|
+
|
|
52
|
+
The source, contracts, and release process are maintained in the
|
|
53
|
+
[JHarness repository](https://github.com/Ezio2000/jharness).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# jharness-repository
|
|
2
|
+
|
|
3
|
+
Official in-memory, SQLite, MySQL, and Redis implementations of the JHarness
|
|
4
|
+
`RunRepository` protocol.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
# Memory and SQLite only
|
|
8
|
+
uv add jharness-repository
|
|
9
|
+
|
|
10
|
+
# Add exactly the remote drivers the application uses
|
|
11
|
+
uv add "jharness-repository[mysql]"
|
|
12
|
+
uv add "jharness-repository[redis]"
|
|
13
|
+
uv add "jharness-repository[mysql,redis]"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from jharness.repository import MemoryRunRepository
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The base installation has no database-driver dependency. `MemoryRunRepository` needs
|
|
21
|
+
no service, and SQLite uses Python's embedded `sqlite3` module. MySQL and Redis load
|
|
22
|
+
their selected client only when that backend is initialized; there is no fallback or
|
|
23
|
+
compatibility driver. Their services remain supplied by the application, while the
|
|
24
|
+
JHarness development suite exercises them in disposable Docker containers.
|
|
25
|
+
|
|
26
|
+
The source, contracts, and release process are maintained in the
|
|
27
|
+
[JHarness repository](https://github.com/Ezio2000/jharness).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jharness-repository"
|
|
3
|
+
version = "0.2.2"
|
|
4
|
+
description = "Memory, SQLite, MySQL, and Redis checkpoint repositories for JHarness"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "JHarness contributors" }]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"jharness-kernel==0.2.2",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
mysql = ["pymysql[rsa]>=1.2.0"]
|
|
27
|
+
redis = ["redis>=8.0.1"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Documentation = "https://github.com/Ezio2000/jharness/tree/main/docs"
|
|
31
|
+
Repository = "https://github.com/Ezio2000/jharness.git"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/jharness"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = ["src/jharness/repository", "LICENSE", "README.md", "pyproject.toml"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Official checkpoint repository implementations for JHarness."""
|
|
2
|
+
|
|
3
|
+
from .memory import MemoryRunRepository
|
|
4
|
+
from .mysql import MySQLRunRepository
|
|
5
|
+
from .redis import RedisRunRepository
|
|
6
|
+
from .sqlite import SQLiteRunRepository
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"MemoryRunRepository",
|
|
10
|
+
"MySQLRunRepository",
|
|
11
|
+
"RedisRunRepository",
|
|
12
|
+
"SQLiteRunRepository",
|
|
13
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Canonical checkpoint encoding shared by repository adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from hashlib import sha256
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from jharness.kernel import Checkpoint, ProtocolError, RepositoryError
|
|
11
|
+
from jharness.kernel.wire import decode_checkpoint as decode_wire_checkpoint
|
|
12
|
+
from jharness.kernel.wire import encode_checkpoint as encode_wire_checkpoint
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class EncodedCheckpoint:
|
|
17
|
+
"""The indexed fields and canonical bytes for one checkpoint."""
|
|
18
|
+
|
|
19
|
+
checkpoint_id: str
|
|
20
|
+
run_id: str
|
|
21
|
+
revision: int
|
|
22
|
+
payload: bytes
|
|
23
|
+
digest: bytes
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def expected_revision(self) -> int | None:
|
|
27
|
+
"""Return the head revision required immediately before this checkpoint."""
|
|
28
|
+
return self.revision - 1 if self.revision else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def encode_checkpoint(checkpoint: Checkpoint) -> EncodedCheckpoint:
|
|
32
|
+
"""Encode a checkpoint into stable JSON bytes and a content fingerprint."""
|
|
33
|
+
if not isinstance(cast(object, checkpoint), Checkpoint):
|
|
34
|
+
raise TypeError("checkpoint must be a Checkpoint")
|
|
35
|
+
checkpoint_id = str.__str__(checkpoint.id)
|
|
36
|
+
run_id = str.__str__(checkpoint.snapshot.context.run_id)
|
|
37
|
+
if not checkpoint_id:
|
|
38
|
+
raise ValueError("checkpoint id must not be empty")
|
|
39
|
+
if not run_id:
|
|
40
|
+
raise ValueError("run id must not be empty")
|
|
41
|
+
payload = json.dumps(
|
|
42
|
+
encode_wire_checkpoint(checkpoint),
|
|
43
|
+
allow_nan=False,
|
|
44
|
+
ensure_ascii=False,
|
|
45
|
+
separators=(",", ":"),
|
|
46
|
+
sort_keys=True,
|
|
47
|
+
).encode("utf-8")
|
|
48
|
+
return EncodedCheckpoint(
|
|
49
|
+
checkpoint_id=checkpoint_id,
|
|
50
|
+
run_id=run_id,
|
|
51
|
+
revision=checkpoint.snapshot.revision,
|
|
52
|
+
payload=payload,
|
|
53
|
+
digest=sha256(payload).digest(),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def decode_checkpoint(payload: bytes) -> Checkpoint:
|
|
58
|
+
"""Decode stored checkpoint bytes, reporting malformed storage uniformly."""
|
|
59
|
+
if type(payload) is not bytes:
|
|
60
|
+
raise TypeError("checkpoint payload must be bytes")
|
|
61
|
+
try:
|
|
62
|
+
document: object = json.loads(payload)
|
|
63
|
+
return decode_wire_checkpoint(document)
|
|
64
|
+
except (
|
|
65
|
+
OverflowError,
|
|
66
|
+
RecursionError,
|
|
67
|
+
UnicodeDecodeError,
|
|
68
|
+
ProtocolError,
|
|
69
|
+
TypeError,
|
|
70
|
+
ValueError,
|
|
71
|
+
) as exc:
|
|
72
|
+
raise RepositoryError("stored checkpoint payload is invalid") from exc
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Process-local, multi-run checkpoint repository."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from threading import Lock
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from jharness.kernel import Checkpoint, RepositoryError, RevisionConflict
|
|
9
|
+
|
|
10
|
+
from ._codec import EncodedCheckpoint, decode_checkpoint, encode_checkpoint
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MemoryRunRepository:
|
|
14
|
+
"""Thread-safe in-memory CAS repository for any number of runs."""
|
|
15
|
+
|
|
16
|
+
__slots__ = ("_by_id", "_heads", "_lock")
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._heads: dict[str, EncodedCheckpoint] = {}
|
|
20
|
+
self._by_id: dict[str, bytes] = {}
|
|
21
|
+
self._lock = Lock()
|
|
22
|
+
|
|
23
|
+
async def commit(self, checkpoint: Checkpoint) -> None:
|
|
24
|
+
"""Atomically advance one run head, or accept an exact prior retry."""
|
|
25
|
+
encoded = encode_checkpoint(checkpoint)
|
|
26
|
+
with self._lock:
|
|
27
|
+
existing_digest = self._by_id.get(encoded.checkpoint_id)
|
|
28
|
+
if existing_digest is not None:
|
|
29
|
+
if existing_digest == encoded.digest:
|
|
30
|
+
return
|
|
31
|
+
raise RepositoryError(
|
|
32
|
+
f"checkpoint id {encoded.checkpoint_id!r} was reused with new content"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
head = self._heads.get(encoded.run_id)
|
|
36
|
+
actual_revision = None if head is None else head.revision
|
|
37
|
+
if actual_revision != encoded.expected_revision:
|
|
38
|
+
raise RevisionConflict(
|
|
39
|
+
encoded.run_id,
|
|
40
|
+
encoded.expected_revision,
|
|
41
|
+
actual_revision,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
previous_head = self._heads.get(encoded.run_id)
|
|
45
|
+
try:
|
|
46
|
+
self._by_id[encoded.checkpoint_id] = encoded.digest
|
|
47
|
+
self._heads[encoded.run_id] = encoded
|
|
48
|
+
except BaseException:
|
|
49
|
+
self._by_id.pop(encoded.checkpoint_id, None)
|
|
50
|
+
if previous_head is None:
|
|
51
|
+
self._heads.pop(encoded.run_id, None)
|
|
52
|
+
else:
|
|
53
|
+
self._heads[encoded.run_id] = previous_head
|
|
54
|
+
raise
|
|
55
|
+
|
|
56
|
+
async def get_head(self, run_id: str) -> Checkpoint | None:
|
|
57
|
+
"""Return the authoritative checkpoint for a run, if one exists."""
|
|
58
|
+
run_id = _validate_run_id(run_id)
|
|
59
|
+
with self._lock:
|
|
60
|
+
head = self._heads.get(run_id)
|
|
61
|
+
return None if head is None else decode_checkpoint(head.payload)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _validate_run_id(run_id: str) -> str:
|
|
65
|
+
if not isinstance(cast(object, run_id), str):
|
|
66
|
+
raise TypeError("run_id must be a string")
|
|
67
|
+
run_id = str.__str__(run_id)
|
|
68
|
+
if not run_id:
|
|
69
|
+
raise ValueError("run_id must not be empty")
|
|
70
|
+
return run_id
|