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.
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from persistent-bundles!"
@@ -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
@@ -0,0 +1,2 @@
1
+ class IncompatibleBundleVersionError(Exception):
2
+ """Raised when a bundle's saved class version cannot be loaded by the current class."""
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,2 @@
1
+ def is_same_major_semver(s1: str, s2: str) -> bool:
2
+ return s1.split(".")[0] == s2.split(".")[0]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.14
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any