persistent-bundles 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.
- persistent_bundles/__init__.py +2 -0
- persistent_bundles/api.py +68 -0
- persistent_bundles/exceptions.py +2 -0
- persistent_bundles/py.typed +0 -0
- persistent_bundles/types.py +21 -0
- persistent_bundles/utils.py +2 -0
- persistent_bundles-0.1.0.dist-info/METADATA +78 -0
- persistent_bundles-0.1.0.dist-info/RECORD +9 -0
- persistent_bundles-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from persistent_bundles.exceptions import IncompatibleBundleVersionError
|
|
7
|
+
from persistent_bundles.types import Loadable, Savable
|
|
8
|
+
from persistent_bundles.utils import is_same_major_semver
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def save_bundle(
|
|
12
|
+
obj: Savable,
|
|
13
|
+
path: Path,
|
|
14
|
+
metadata: Mapping[str, Any] | None = None, # pyright: ignore[reportExplicitAny]
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Save an object and the metadata needed to load it later."""
|
|
17
|
+
assert str(path).endswith(".bundle")
|
|
18
|
+
|
|
19
|
+
object_path = path / "object"
|
|
20
|
+
object_path.mkdir(parents=True)
|
|
21
|
+
obj.save(object_path)
|
|
22
|
+
|
|
23
|
+
with (path / "manifest.json").open("w") as f:
|
|
24
|
+
json.dump(
|
|
25
|
+
{
|
|
26
|
+
"class_name": obj.__class__.__name__,
|
|
27
|
+
"class_version": obj.get_class_version(),
|
|
28
|
+
},
|
|
29
|
+
f,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if metadata is not None:
|
|
33
|
+
with (path / "metadata.json").open("w") as f:
|
|
34
|
+
json.dump(metadata, f)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_bundle(
|
|
38
|
+
path: Path,
|
|
39
|
+
class_mapping: Mapping[str, type[Loadable]],
|
|
40
|
+
accept_incompatible_classes: bool = False,
|
|
41
|
+
) -> tuple[Loadable, Mapping[str, Any]]: # pyright: ignore[reportExplicitAny]
|
|
42
|
+
"""Load a bundle object, returning both the object and metadata."""
|
|
43
|
+
assert str(path).endswith(".bundle")
|
|
44
|
+
|
|
45
|
+
with (path / "manifest.json").open("r") as f:
|
|
46
|
+
manifest = json.load(f)
|
|
47
|
+
|
|
48
|
+
class_name = manifest["class_name"]
|
|
49
|
+
class_to_be_loaded = class_mapping[class_name]
|
|
50
|
+
current_class_version = class_to_be_loaded.get_class_version()
|
|
51
|
+
saved_class_version = manifest["class_version"]
|
|
52
|
+
|
|
53
|
+
if not accept_incompatible_classes and not is_same_major_semver(
|
|
54
|
+
saved_class_version, current_class_version
|
|
55
|
+
):
|
|
56
|
+
raise IncompatibleBundleVersionError(
|
|
57
|
+
"The loaded bundle does not have the same major class version as the current class"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
obj = class_to_be_loaded.load(path / "object")
|
|
61
|
+
|
|
62
|
+
if (path / "metadata.json").exists():
|
|
63
|
+
with (path / "metadata.json").open("r") as f:
|
|
64
|
+
metadata = json.load(f)
|
|
65
|
+
else:
|
|
66
|
+
metadata = {}
|
|
67
|
+
|
|
68
|
+
return obj, metadata
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Protocol, Self
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Savable(Protocol):
|
|
6
|
+
def save(self, path: Path) -> None: ...
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def get_class_version(cls) -> str:
|
|
10
|
+
"""Return the class version according to semantic versioning. A differing major version indicates incompatible states."""
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Loadable(Protocol):
|
|
15
|
+
@classmethod
|
|
16
|
+
def load(cls, path: Path) -> Self: ...
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_class_version(cls) -> str:
|
|
20
|
+
"""Return the class version according to semantic versioning. A differing major version indicates incompatible states."""
|
|
21
|
+
...
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: persistent-bundles
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Save and load versioned Python object bundles with metadata.
|
|
5
|
+
Keywords: bundles,persistence,serialization,versioning
|
|
6
|
+
Author: Valter Schütz
|
|
7
|
+
Author-email: Valter Schütz <valterschutz@proton.me>
|
|
8
|
+
License-Expression: Unlicense
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.13
|
|
16
|
+
Project-URL: Issues, https://github.com/valterschutz/persistent-bundles/issues
|
|
17
|
+
Project-URL: Repository, https://github.com/valterschutz/persistent-bundles
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# persistent-bundles
|
|
21
|
+
|
|
22
|
+
Save and load versioned Python object bundles with optional metadata.
|
|
23
|
+
|
|
24
|
+
`persistent-bundles` is a small library for objects that manage their own on-disk persistence. It writes a bundle directory containing the saved object, a manifest with the object's class name and semantic version, and optional JSON metadata.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install persistent-bundles
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import json
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Self, override
|
|
38
|
+
|
|
39
|
+
from persistent_bundles.api import load_bundle, save_bundle
|
|
40
|
+
from persistent_bundles.types import Loadable, Savable
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MyObject(Savable, Loadable):
|
|
44
|
+
def __init__(self, number: int) -> None:
|
|
45
|
+
self.number = number
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def save(self, path: Path) -> None:
|
|
49
|
+
with (path / "number.json").open("w") as f:
|
|
50
|
+
json.dump({"number": self.number}, f)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
@override
|
|
54
|
+
def load(cls, path: Path) -> Self:
|
|
55
|
+
with (path / "number.json").open("r") as f:
|
|
56
|
+
data = json.load(f)
|
|
57
|
+
return cls(data["number"])
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
@override
|
|
61
|
+
def get_class_version(cls) -> str:
|
|
62
|
+
return "1.0.0"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
path = Path("example.bundle")
|
|
66
|
+
save_bundle(MyObject(42), path, metadata={"created_by": "example"})
|
|
67
|
+
|
|
68
|
+
obj, metadata = load_bundle(path, {"MyObject": MyObject})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Bundles require a `.bundle` path suffix. When loading, the saved class version must have the same semantic-version major number as the current class version.
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv run pytest
|
|
77
|
+
uv build
|
|
78
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
persistent_bundles/__init__.py,sha256=K9z1tbgmrh1vHYkhcKwJuBRbsvR8zg2y8R0usdokrjQ,64
|
|
2
|
+
persistent_bundles/api.py,sha256=u2HY5MvfVChjsvHAkrtlcCVxB28n_F5hZ82cBKpgS3M,2132
|
|
3
|
+
persistent_bundles/exceptions.py,sha256=PyVFA0EEK8ki2C50dhWxh_7nVqkbWMoN3ILfMqEEYOc,141
|
|
4
|
+
persistent_bundles/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
persistent_bundles/types.py,sha256=SncsC3g-HA0lLR3W7TY7D72HBrWXnM2szDOhok5SCfw,616
|
|
6
|
+
persistent_bundles/utils.py,sha256=CUDZAW2Aa5IjhUfFm_dFGkw9SKdI1tnJPPHQjH8GKtg,100
|
|
7
|
+
persistent_bundles-0.1.0.dist-info/WHEEL,sha256=NHRAbdxxzyL9K3IO2LjmlNqKSyPZnKv2BD16YYVKo18,79
|
|
8
|
+
persistent_bundles-0.1.0.dist-info/METADATA,sha256=EOeVjKHKkNH57lXwOTow7FGFu6CXj6Bnf1w_ea_0nLk,2325
|
|
9
|
+
persistent_bundles-0.1.0.dist-info/RECORD,,
|