dazzle-lib 0.1.0__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.
- dazzle_lib-0.1.0/LICENSE +21 -0
- dazzle_lib-0.1.0/PKG-INFO +118 -0
- dazzle_lib-0.1.0/README.md +88 -0
- dazzle_lib-0.1.0/dazzle_lib/__init__.py +51 -0
- dazzle_lib-0.1.0/dazzle_lib/__main__.py +11 -0
- dazzle_lib-0.1.0/dazzle_lib/_version.py +92 -0
- dazzle_lib-0.1.0/dazzle_lib/exceptions.py +43 -0
- dazzle_lib-0.1.0/dazzle_lib/mixins.py +42 -0
- dazzle_lib-0.1.0/dazzle_lib/payloads.py +127 -0
- dazzle_lib-0.1.0/dazzle_lib/protocols.py +60 -0
- dazzle_lib-0.1.0/dazzle_lib.egg-info/PKG-INFO +118 -0
- dazzle_lib-0.1.0/dazzle_lib.egg-info/SOURCES.txt +19 -0
- dazzle_lib-0.1.0/dazzle_lib.egg-info/dependency_links.txt +1 -0
- dazzle_lib-0.1.0/dazzle_lib.egg-info/requires.txt +4 -0
- dazzle_lib-0.1.0/dazzle_lib.egg-info/top_level.txt +1 -0
- dazzle_lib-0.1.0/pyproject.toml +62 -0
- dazzle_lib-0.1.0/setup.cfg +4 -0
- dazzle_lib-0.1.0/tests/test_charter.py +96 -0
- dazzle_lib-0.1.0/tests/test_import_stability.py +73 -0
- dazzle_lib-0.1.0/tests/test_protocols_and_mixin.py +84 -0
- dazzle_lib-0.1.0/tests/test_version.py +50 -0
dazzle_lib-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dustin Darcy
|
|
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,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dazzle-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The DazzleLib bedrock: shared Protocols, TypedDict payload schemas, and exception root for the dazzle-* library stack. Stdlib-only by charter.
|
|
5
|
+
Author-email: djdarcy <djdarcy@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DazzleLib/dazzle-lib
|
|
8
|
+
Project-URL: Repository, https://github.com/DazzleLib/dazzle-lib
|
|
9
|
+
Project-URL: Issues, https://github.com/DazzleLib/dazzle-lib/issues
|
|
10
|
+
Keywords: dazzlelib,protocols,typeddict,serialization,bedrock,stack
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# dazzle-lib
|
|
32
|
+
|
|
33
|
+
**The DazzleLib stack's bedrock: shared Protocols, TypedDict payload schemas, and the exception root.**
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/dazzle-lib/)
|
|
36
|
+
[](https://pypi.org/project/dazzle-lib/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](docs/platform-support.md)
|
|
39
|
+
|
|
40
|
+
Every `dazzle-*` library ([the stack](https://github.com/DazzleLib/.github/blob/main/docs/STACK-MAP.md)) builds on this package: it defines what stack objects can be expected to do (view themselves, serialize themselves) and what shapes cross-layer payloads have. **Types only** -- by charter this package contains no I/O, no path handling, no platform probing, and no behavior, forever.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install dazzle-lib
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What's inside (all of it)
|
|
47
|
+
|
|
48
|
+
| Module | Contents |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `dazzle_lib.protocols` | `Viewable` (`summary()`/`__str__`), `Serializable` (`to_dict`/`from_dict`/`to_json`, `SCHEMA_VERSION`) -- structural `Protocol`s, `runtime_checkable`, nothing is forced to subclass |
|
|
51
|
+
| `dazzle_lib.payloads` | The cross-layer TypedDict schemas: `FileMetadataDict`, `TimestampsDict`, `WindowsMetadataDict`, `UnixMetadataDict`, `LinkTargetDict`, `HashResultDict` -- mirroring what `dazzle-filekit` actually produces |
|
|
52
|
+
| `dazzle_lib.exceptions` | `DazzleError` root + per-domain bases (`PathIdentityError`, `FileOperationError`, `LinkError`, `PreserveError`) |
|
|
53
|
+
| `dazzle_lib.mixins` | `DazzleDataMixin` -- derives `to_json`/`summary`/`__str__` from your `to_dict` |
|
|
54
|
+
|
|
55
|
+
## The idea: the dict is the interface
|
|
56
|
+
|
|
57
|
+
Rich objects in upper layers know how to become plain dicts; lower-layer functions take and return those dicts. The TypedDicts here are the agreed shapes, so a manifest object in `dazzle-preservelib` and a metadata collector in `dazzle-filekit` speak the same payload without sharing a class hierarchy:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from dataclasses import dataclass
|
|
61
|
+
from dazzle_lib import DazzleDataMixin, Serializable, FileMetadataDict
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class TransferResult(DazzleDataMixin):
|
|
65
|
+
SCHEMA_VERSION = 1
|
|
66
|
+
path: str
|
|
67
|
+
metadata: FileMetadataDict
|
|
68
|
+
|
|
69
|
+
def to_dict(self):
|
|
70
|
+
return {"schema_version": self.SCHEMA_VERSION,
|
|
71
|
+
"path": self.path, "metadata": dict(self.metadata)}
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data):
|
|
75
|
+
return cls(path=data["path"], metadata=data["metadata"])
|
|
76
|
+
|
|
77
|
+
result = TransferResult("a.txt", {"mode": 0o644, "size": 10, "timestamps": {}})
|
|
78
|
+
assert isinstance(result, Serializable) # structural -- no subclassing needed
|
|
79
|
+
print(result.summary()) # one-liner for logs
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
And one catchable root for the whole stack:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from dazzle_lib import DazzleError
|
|
86
|
+
try:
|
|
87
|
+
... # any dazzle-* library call
|
|
88
|
+
except DazzleError as e:
|
|
89
|
+
... # caught, whichever layer raised it
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## The charter (enforced by tests)
|
|
93
|
+
|
|
94
|
+
This package is **stdlib-only forever** and contains **no behavior**. `tests/test_charter.py` fails on any banned import (`os`, `shutil`, `pathlib`, `subprocess`, ...) anywhere in the package -- a PR that needs to weaken that test is adding something that belongs in a higher layer. Admission follows the **rule of two**: a Protocol or TypedDict enters the bedrock only when two or more stack libraries need it.
|
|
95
|
+
|
|
96
|
+
## The stack
|
|
97
|
+
|
|
98
|
+
| Layer | Library | Role |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| B | **dazzle-lib** (this) | bedrock contracts |
|
|
101
|
+
| L0 | [dazzle-unctools](https://github.com/DazzleLib/UNCtools) | path identity (UNC/drive/origin) |
|
|
102
|
+
| L1 | [dazzle-filekit](https://github.com/DazzleLib/dazzle-filekit) | filesystem primitives |
|
|
103
|
+
| L2 | dazzle-linklib *(planned)* | link serialization |
|
|
104
|
+
| L3 | dazzle-preservelib *(planned)* | operation orchestration |
|
|
105
|
+
| ⊥ | [dazzle-treelib](https://github.com/DazzleLib/dazzle-tree-lib) | traversal engine |
|
|
106
|
+
|
|
107
|
+
Full architecture contract: [STACK-MAP.md](https://github.com/DazzleLib/.github/blob/main/docs/STACK-MAP.md). API stability policy: [docs/api-stability.md](docs/api-stability.md).
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install -e ".[dev]"
|
|
113
|
+
python -m pytest tests/ -v
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT -- the bedrock sits beneath MIT and GPL stack members alike, so it carries the permissive license.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# dazzle-lib
|
|
2
|
+
|
|
3
|
+
**The DazzleLib stack's bedrock: shared Protocols, TypedDict payload schemas, and the exception root.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/dazzle-lib/)
|
|
6
|
+
[](https://pypi.org/project/dazzle-lib/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](docs/platform-support.md)
|
|
9
|
+
|
|
10
|
+
Every `dazzle-*` library ([the stack](https://github.com/DazzleLib/.github/blob/main/docs/STACK-MAP.md)) builds on this package: it defines what stack objects can be expected to do (view themselves, serialize themselves) and what shapes cross-layer payloads have. **Types only** -- by charter this package contains no I/O, no path handling, no platform probing, and no behavior, forever.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install dazzle-lib
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What's inside (all of it)
|
|
17
|
+
|
|
18
|
+
| Module | Contents |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `dazzle_lib.protocols` | `Viewable` (`summary()`/`__str__`), `Serializable` (`to_dict`/`from_dict`/`to_json`, `SCHEMA_VERSION`) -- structural `Protocol`s, `runtime_checkable`, nothing is forced to subclass |
|
|
21
|
+
| `dazzle_lib.payloads` | The cross-layer TypedDict schemas: `FileMetadataDict`, `TimestampsDict`, `WindowsMetadataDict`, `UnixMetadataDict`, `LinkTargetDict`, `HashResultDict` -- mirroring what `dazzle-filekit` actually produces |
|
|
22
|
+
| `dazzle_lib.exceptions` | `DazzleError` root + per-domain bases (`PathIdentityError`, `FileOperationError`, `LinkError`, `PreserveError`) |
|
|
23
|
+
| `dazzle_lib.mixins` | `DazzleDataMixin` -- derives `to_json`/`summary`/`__str__` from your `to_dict` |
|
|
24
|
+
|
|
25
|
+
## The idea: the dict is the interface
|
|
26
|
+
|
|
27
|
+
Rich objects in upper layers know how to become plain dicts; lower-layer functions take and return those dicts. The TypedDicts here are the agreed shapes, so a manifest object in `dazzle-preservelib` and a metadata collector in `dazzle-filekit` speak the same payload without sharing a class hierarchy:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from dazzle_lib import DazzleDataMixin, Serializable, FileMetadataDict
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class TransferResult(DazzleDataMixin):
|
|
35
|
+
SCHEMA_VERSION = 1
|
|
36
|
+
path: str
|
|
37
|
+
metadata: FileMetadataDict
|
|
38
|
+
|
|
39
|
+
def to_dict(self):
|
|
40
|
+
return {"schema_version": self.SCHEMA_VERSION,
|
|
41
|
+
"path": self.path, "metadata": dict(self.metadata)}
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_dict(cls, data):
|
|
45
|
+
return cls(path=data["path"], metadata=data["metadata"])
|
|
46
|
+
|
|
47
|
+
result = TransferResult("a.txt", {"mode": 0o644, "size": 10, "timestamps": {}})
|
|
48
|
+
assert isinstance(result, Serializable) # structural -- no subclassing needed
|
|
49
|
+
print(result.summary()) # one-liner for logs
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
And one catchable root for the whole stack:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from dazzle_lib import DazzleError
|
|
56
|
+
try:
|
|
57
|
+
... # any dazzle-* library call
|
|
58
|
+
except DazzleError as e:
|
|
59
|
+
... # caught, whichever layer raised it
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## The charter (enforced by tests)
|
|
63
|
+
|
|
64
|
+
This package is **stdlib-only forever** and contains **no behavior**. `tests/test_charter.py` fails on any banned import (`os`, `shutil`, `pathlib`, `subprocess`, ...) anywhere in the package -- a PR that needs to weaken that test is adding something that belongs in a higher layer. Admission follows the **rule of two**: a Protocol or TypedDict enters the bedrock only when two or more stack libraries need it.
|
|
65
|
+
|
|
66
|
+
## The stack
|
|
67
|
+
|
|
68
|
+
| Layer | Library | Role |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| B | **dazzle-lib** (this) | bedrock contracts |
|
|
71
|
+
| L0 | [dazzle-unctools](https://github.com/DazzleLib/UNCtools) | path identity (UNC/drive/origin) |
|
|
72
|
+
| L1 | [dazzle-filekit](https://github.com/DazzleLib/dazzle-filekit) | filesystem primitives |
|
|
73
|
+
| L2 | dazzle-linklib *(planned)* | link serialization |
|
|
74
|
+
| L3 | dazzle-preservelib *(planned)* | operation orchestration |
|
|
75
|
+
| ⊥ | [dazzle-treelib](https://github.com/DazzleLib/dazzle-tree-lib) | traversal engine |
|
|
76
|
+
|
|
77
|
+
Full architecture contract: [STACK-MAP.md](https://github.com/DazzleLib/.github/blob/main/docs/STACK-MAP.md). API stability policy: [docs/api-stability.md](docs/api-stability.md).
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install -e ".[dev]"
|
|
83
|
+
python -m pytest tests/ -v
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT -- the bedrock sits beneath MIT and GPL stack members alike, so it carries the permissive license.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""dazzle-lib -- the DazzleLib stack's bedrock.
|
|
2
|
+
|
|
3
|
+
Shared Protocols, TypedDict payload schemas, and the exception root that every
|
|
4
|
+
``dazzle-*`` library builds on. Types only: this package is stdlib-only forever
|
|
5
|
+
and, by charter, contains no I/O, no path handling, no platform probing, and no
|
|
6
|
+
"utils". See the architecture contract:
|
|
7
|
+
https://github.com/DazzleLib/.github/blob/main/docs/STACK-MAP.md
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._version import PIP_VERSION, __app_name__, __version__
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
DazzleError,
|
|
13
|
+
FileOperationError,
|
|
14
|
+
LinkError,
|
|
15
|
+
PathIdentityError,
|
|
16
|
+
PreserveError,
|
|
17
|
+
)
|
|
18
|
+
from .mixins import DazzleDataMixin
|
|
19
|
+
from .payloads import (
|
|
20
|
+
FileMetadataDict,
|
|
21
|
+
HashResultDict,
|
|
22
|
+
LinkTargetDict,
|
|
23
|
+
TimestampsDict,
|
|
24
|
+
UnixMetadataDict,
|
|
25
|
+
WindowsMetadataDict,
|
|
26
|
+
)
|
|
27
|
+
from .protocols import Serializable, Viewable
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"__version__",
|
|
31
|
+
"__app_name__",
|
|
32
|
+
"PIP_VERSION",
|
|
33
|
+
# protocols
|
|
34
|
+
"Viewable",
|
|
35
|
+
"Serializable",
|
|
36
|
+
# payload schemas
|
|
37
|
+
"TimestampsDict",
|
|
38
|
+
"WindowsMetadataDict",
|
|
39
|
+
"UnixMetadataDict",
|
|
40
|
+
"FileMetadataDict",
|
|
41
|
+
"LinkTargetDict",
|
|
42
|
+
"HashResultDict",
|
|
43
|
+
# exceptions
|
|
44
|
+
"DazzleError",
|
|
45
|
+
"PathIdentityError",
|
|
46
|
+
"FileOperationError",
|
|
47
|
+
"LinkError",
|
|
48
|
+
"PreserveError",
|
|
49
|
+
# mixin
|
|
50
|
+
"DazzleDataMixin",
|
|
51
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""``python -m dazzle_lib`` -- print version and charter (a library, not a tool)."""
|
|
2
|
+
|
|
3
|
+
from ._version import DISPLAY_VERSION, __app_name__
|
|
4
|
+
|
|
5
|
+
if __name__ == "__main__":
|
|
6
|
+
print(f"{__app_name__} {DISPLAY_VERSION}")
|
|
7
|
+
print(
|
|
8
|
+
"The DazzleLib stack's bedrock: Protocols, TypedDict payload schemas, "
|
|
9
|
+
"and the exception root.\nTypes only -- by charter this package "
|
|
10
|
+
"contains no behavior. https://github.com/DazzleLib/dazzle-lib"
|
|
11
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version information for dazzle-lib.
|
|
3
|
+
|
|
4
|
+
This file is the canonical source for version numbers.
|
|
5
|
+
The __version__ string is automatically updated by git hooks
|
|
6
|
+
with build metadata (branch, build number, date, commit hash).
|
|
7
|
+
|
|
8
|
+
Format: MAJOR.MINOR.PATCH[-PHASE]_BRANCH_BUILD-YYYYMMDD-COMMITHASH
|
|
9
|
+
Example: 0.1.0_main_1-20260101-a1b2c3d4
|
|
10
|
+
|
|
11
|
+
Version levels:
|
|
12
|
+
PROJECT_PHASE: Global project maturity (prealpha -> alpha -> beta -> stable).
|
|
13
|
+
Changes rarely, when the overall project hits a threshold.
|
|
14
|
+
PHASE: Per-MINOR feature set maturity (alpha -> beta -> "" for stable).
|
|
15
|
+
Drops when a MINOR's feature set is complete.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# Version components - edit these for version bumps
|
|
19
|
+
MAJOR = 0
|
|
20
|
+
MINOR = 1
|
|
21
|
+
PATCH = 0
|
|
22
|
+
PHASE = "" # Per-MINOR feature set: "" (stable), "alpha", "beta", "rc1", etc.
|
|
23
|
+
|
|
24
|
+
# Project-level phase (independent of version phase)
|
|
25
|
+
PROJECT_PHASE = "" # "prealpha", "alpha", "beta", "stable", or ""
|
|
26
|
+
|
|
27
|
+
# Auto-updated by git hooks - do not edit manually
|
|
28
|
+
__version__ = "0.1.0_main_5-20260611-b3174573"
|
|
29
|
+
__app_name__ = "dazzle-lib"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_version():
|
|
33
|
+
"""Return the full version string including branch and build info."""
|
|
34
|
+
return __version__
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_base_version():
|
|
38
|
+
"""Return the semantic version string (MAJOR.MINOR.PATCH[-PHASE])."""
|
|
39
|
+
if "_" in __version__:
|
|
40
|
+
return __version__.split("_")[0]
|
|
41
|
+
base = f"{MAJOR}.{MINOR}.{PATCH}"
|
|
42
|
+
if PHASE:
|
|
43
|
+
base = f"{base}-{PHASE}"
|
|
44
|
+
return base
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_display_version():
|
|
48
|
+
"""Return a human-friendly version string with project phase.
|
|
49
|
+
|
|
50
|
+
Example: 'PREALPHA 0.1.0-alpha' or 'BETA 0.5.1' or '1.0.0'
|
|
51
|
+
"""
|
|
52
|
+
base = get_base_version()
|
|
53
|
+
if PROJECT_PHASE and PROJECT_PHASE != "stable":
|
|
54
|
+
return f"{PROJECT_PHASE.upper()} {base}"
|
|
55
|
+
return base
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_pip_version():
|
|
59
|
+
"""
|
|
60
|
+
Return PEP 440 compliant version for pip/setuptools.
|
|
61
|
+
|
|
62
|
+
Converts our version format to PEP 440:
|
|
63
|
+
- Main branch: 0.1.0_main_3-20260404-hash -> 0.1.0
|
|
64
|
+
- Dev branch: 0.1.0_dev_3-20260404-hash -> 0.1.0.dev3
|
|
65
|
+
- Alpha: 0.1.0-alpha_main_3 -> 0.1.0a0
|
|
66
|
+
"""
|
|
67
|
+
base = f"{MAJOR}.{MINOR}.{PATCH}"
|
|
68
|
+
|
|
69
|
+
# Map phase to PEP 440 pre-release segment
|
|
70
|
+
phase_map = {"alpha": "a0", "beta": "b0"}
|
|
71
|
+
if PHASE:
|
|
72
|
+
base += phase_map.get(PHASE, PHASE)
|
|
73
|
+
|
|
74
|
+
if "_" not in __version__:
|
|
75
|
+
return base
|
|
76
|
+
|
|
77
|
+
parts = __version__.split("_")
|
|
78
|
+
branch = parts[1] if len(parts) > 1 else "unknown"
|
|
79
|
+
|
|
80
|
+
if branch == "main":
|
|
81
|
+
return base
|
|
82
|
+
else:
|
|
83
|
+
build_info = "_".join(parts[2:]) if len(parts) > 2 else ""
|
|
84
|
+
build_num = build_info.split("-")[0] if "-" in build_info else "0"
|
|
85
|
+
return f"{base}.dev{build_num}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# For convenience in imports
|
|
89
|
+
VERSION = get_version()
|
|
90
|
+
BASE_VERSION = get_base_version()
|
|
91
|
+
PIP_VERSION = get_pip_version()
|
|
92
|
+
DISPLAY_VERSION = get_display_version()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""The DazzleLib exception bedrock.
|
|
2
|
+
|
|
3
|
+
One catchable root (:class:`DazzleError`) for the whole stack, plus one base
|
|
4
|
+
per layer domain. Each stack library derives ITS OWN exceptions from the
|
|
5
|
+
matching domain base (e.g. dazzle-preservelib's ``ManifestVersionError`` would
|
|
6
|
+
subclass :class:`PreserveError`), so consumers can choose their catch
|
|
7
|
+
granularity: a specific error, a domain, or the whole stack.
|
|
8
|
+
|
|
9
|
+
Charter reminder: these are plain exception types. No behavior beyond
|
|
10
|
+
``__init__``-style state, no I/O.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DazzleError",
|
|
15
|
+
"PathIdentityError",
|
|
16
|
+
"FileOperationError",
|
|
17
|
+
"LinkError",
|
|
18
|
+
"PreserveError",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DazzleError(Exception):
|
|
23
|
+
"""Root of every exception raised by a DazzleLib stack library."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PathIdentityError(DazzleError):
|
|
27
|
+
"""Domain base for path-identity failures (dazzle-unctools, L0):
|
|
28
|
+
UNC/drive mapping, origin classification, identity probing."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FileOperationError(DazzleError):
|
|
32
|
+
"""Domain base for filesystem-primitive failures (dazzle-filekit, L1):
|
|
33
|
+
copy/move, metadata collect/apply, hashing, link creation."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LinkError(DazzleError):
|
|
37
|
+
"""Domain base for link-serialization failures (dazzle-linklib, L2):
|
|
38
|
+
.dazzlelink parsing, export/import, rebase."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PreserveError(DazzleError):
|
|
42
|
+
"""Domain base for orchestration failures (dazzle-preservelib, L3):
|
|
43
|
+
manifests, transactional copy/move/restore/verify, conflict policy."""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The one permitted mixin: derive presentation from ``to_dict``.
|
|
2
|
+
|
|
3
|
+
:class:`DazzleDataMixin` is convenience, not contract -- objects may satisfy
|
|
4
|
+
the protocols without it. It exists so simple data objects don't each rewrite
|
|
5
|
+
``to_json``/``__str__``/``summary`` plumbing. Anything fancier belongs in the
|
|
6
|
+
object itself, not here (charter: this module stays this small).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
__all__ = ["DazzleDataMixin"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DazzleDataMixin:
|
|
16
|
+
"""Derives ``to_json``, ``summary`` and ``__str__`` from ``to_dict``.
|
|
17
|
+
|
|
18
|
+
The host class supplies ``to_dict()`` (and thereby decides what is
|
|
19
|
+
JSON-safe); the mixin only formats. Combined with a ``from_dict``
|
|
20
|
+
classmethod and a ``SCHEMA_VERSION`` attribute, a host class satisfies
|
|
21
|
+
both :class:`~dazzle_lib.protocols.Serializable` and
|
|
22
|
+
:class:`~dazzle_lib.protocols.Viewable`.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> Dict[str, Any]: # pragma: no cover - host overrides
|
|
26
|
+
raise NotImplementedError(
|
|
27
|
+
f"{type(self).__name__} must implement to_dict() to use DazzleDataMixin"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def to_json(self, *, indent: int = 2, sort_keys: bool = False) -> str:
|
|
31
|
+
"""JSON form of :meth:`to_dict` (``default=str`` catches stragglers)."""
|
|
32
|
+
return json.dumps(
|
|
33
|
+
self.to_dict(), indent=indent, sort_keys=sort_keys, default=str
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def summary(self) -> str:
|
|
37
|
+
"""One-line description: class name plus top-level keys."""
|
|
38
|
+
keys = ", ".join(list(self.to_dict().keys())[:6])
|
|
39
|
+
return f"{type(self).__name__}({keys})"
|
|
40
|
+
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return self.to_json()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""TypedDict payload schemas shared across the DazzleLib stack.
|
|
2
|
+
|
|
3
|
+
These are THE cross-layer payload shapes (STACK-MAP D10): when one stack
|
|
4
|
+
library hands file metadata, timestamps, link descriptions, or hash results to
|
|
5
|
+
another, the value conforms to a shape defined here. Rich objects in upper
|
|
6
|
+
layers serialize themselves INTO these shapes; primitive functions in lower
|
|
7
|
+
layers take and return them directly.
|
|
8
|
+
|
|
9
|
+
The shapes are not invented -- they mirror what ``dazzle-filekit`` actually
|
|
10
|
+
produces today (``collect_file_metadata`` / ``collect_timestamp_info`` /
|
|
11
|
+
``calculate_file_hash``), so adopting them is a typing change, not a behavior
|
|
12
|
+
change. Admission policy (rule of two): a shape lives here only once two or
|
|
13
|
+
more stack libraries need it.
|
|
14
|
+
|
|
15
|
+
Charter reminder: this module is types-only. No I/O, no behavior.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Dict, Optional, TypedDict
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"TimestampsDict",
|
|
22
|
+
"WindowsMetadataDict",
|
|
23
|
+
"UnixMetadataDict",
|
|
24
|
+
"FileMetadataDict",
|
|
25
|
+
"LinkTargetDict",
|
|
26
|
+
"HashResultDict",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TimestampsDict(TypedDict, total=False):
|
|
31
|
+
"""File timestamps, epoch floats plus ISO-8601 projections.
|
|
32
|
+
|
|
33
|
+
Mirrors ``dazzle_filekit.metadata.collect_timestamp_info``. Note that
|
|
34
|
+
``created`` carries ``st_ctime``, which is creation time on Windows but
|
|
35
|
+
inode-change time on Unix -- consumers must not assume birth-time
|
|
36
|
+
semantics cross-platform.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
created: float
|
|
40
|
+
modified: float
|
|
41
|
+
accessed: float
|
|
42
|
+
created_iso: str
|
|
43
|
+
modified_iso: str
|
|
44
|
+
accessed_iso: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WindowsMetadataDict(TypedDict, total=False):
|
|
48
|
+
"""Windows-specific metadata (mirrors filekit's ``_collect_windows_metadata``).
|
|
49
|
+
|
|
50
|
+
With pywin32 available the rich fields are present (``attributes``,
|
|
51
|
+
owner/group + SIDs, ``security_descriptor_sddl``); without it the
|
|
52
|
+
``attrib``-fallback path fills only the boolean flags and
|
|
53
|
+
``attrib_output``. ``security_descriptor_sddl`` may be present-but-None
|
|
54
|
+
when the descriptor could not be stringified.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
attributes: int
|
|
58
|
+
is_hidden: bool
|
|
59
|
+
is_system: bool
|
|
60
|
+
is_readonly: bool
|
|
61
|
+
is_archive: bool
|
|
62
|
+
owner: str
|
|
63
|
+
group: str
|
|
64
|
+
owner_sid: str
|
|
65
|
+
group_sid: str
|
|
66
|
+
security_descriptor_sddl: Optional[str]
|
|
67
|
+
attrib_output: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class UnixMetadataDict(TypedDict, total=False):
|
|
71
|
+
"""Unix-specific metadata (mirrors filekit's unix branch)."""
|
|
72
|
+
|
|
73
|
+
uid: int
|
|
74
|
+
gid: int
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _FileMetadataRequired(TypedDict):
|
|
78
|
+
mode: int
|
|
79
|
+
size: int
|
|
80
|
+
timestamps: TimestampsDict
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FileMetadataDict(_FileMetadataRequired, total=False):
|
|
84
|
+
"""A file's preservable metadata snapshot.
|
|
85
|
+
|
|
86
|
+
Mirrors ``dazzle_filekit.metadata.collect_file_metadata``: ``mode``,
|
|
87
|
+
``size``, and ``timestamps`` are always present; exactly one of
|
|
88
|
+
``windows`` / ``unix`` appears depending on platform; ``xattrs`` (name ->
|
|
89
|
+
base64-encoded value) appears on Unix when extended attributes exist.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
windows: WindowsMetadataDict
|
|
93
|
+
unix: UnixMetadataDict
|
|
94
|
+
xattrs: Dict[str, str]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LinkTargetDict(TypedDict, total=False):
|
|
98
|
+
"""An intrinsic description of a filesystem link, as data.
|
|
99
|
+
|
|
100
|
+
The cross-layer shape for "what is this link": produced by link analysis
|
|
101
|
+
(dazzle-filekit's ``analyze_link``), embedded in serialized link files
|
|
102
|
+
(dazzle-linklib), and consumed by orchestration policy (dazzle-preservelib).
|
|
103
|
+
Intrinsic properties only -- anything relative to a specific operation
|
|
104
|
+
(e.g. "does this link point inside the move destination") is computed at
|
|
105
|
+
the orchestration layer and does NOT belong here.
|
|
106
|
+
|
|
107
|
+
``kind`` is one of ``"symlink"``, ``"junction"``, ``"hardlink"``.
|
|
108
|
+
``raw_target`` is the stored target text exactly as written; ``resolved_target``
|
|
109
|
+
is its absolute resolution (when resolvable). ``target_is_directory`` is
|
|
110
|
+
best-effort (False when the target is missing).
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
kind: str
|
|
114
|
+
raw_target: str
|
|
115
|
+
resolved_target: str
|
|
116
|
+
is_broken: bool
|
|
117
|
+
is_circular: bool
|
|
118
|
+
target_is_directory: bool
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
HashResultDict = Dict[str, str]
|
|
122
|
+
"""Hash results keyed by algorithm name, hex digests as values.
|
|
123
|
+
|
|
124
|
+
Mirrors ``dazzle_filekit.verification.calculate_file_hash`` (e.g.
|
|
125
|
+
``{"sha256": "ab12...", "md5": "cd34..."}``). A plain ``Dict`` alias rather
|
|
126
|
+
than a TypedDict because the key set is open (any hashlib algorithm).
|
|
127
|
+
"""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Structural protocols every DazzleLib stack object is expected to satisfy.
|
|
2
|
+
|
|
3
|
+
These are :class:`typing.Protocol` definitions -- STRUCTURAL contracts, not base
|
|
4
|
+
classes. Nothing in the stack is required to subclass anything here; an object
|
|
5
|
+
satisfies a protocol simply by implementing its methods (and can be checked at
|
|
6
|
+
runtime with ``isinstance`` because both are ``@runtime_checkable``).
|
|
7
|
+
|
|
8
|
+
The design stance (STACK-MAP D10): "the dict is the interface; objects know how
|
|
9
|
+
to become dicts." Rich objects in upper layers (manifest, link-data, results)
|
|
10
|
+
serialize themselves INTO the plain TypedDict payload shapes defined in
|
|
11
|
+
:mod:`dazzle_lib.payloads`; lower-layer functions take and return those dicts.
|
|
12
|
+
|
|
13
|
+
Charter reminder: this module is types-only. No I/O, no behavior.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
__all__ = ["Viewable", "Serializable"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@runtime_checkable
|
|
22
|
+
class Viewable(Protocol):
|
|
23
|
+
"""An object that can present itself to a human.
|
|
24
|
+
|
|
25
|
+
Expectations:
|
|
26
|
+
|
|
27
|
+
- ``summary()`` returns a SHORT one-line description suitable for list
|
|
28
|
+
views, log lines, and progress output.
|
|
29
|
+
- ``__str__`` may be longer (multi-line is fine) but must never raise.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def summary(self) -> str:
|
|
33
|
+
"""One-line human-readable description of this object."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@runtime_checkable
|
|
38
|
+
class Serializable(Protocol):
|
|
39
|
+
"""An object that can round-trip through a plain, JSON-safe dict.
|
|
40
|
+
|
|
41
|
+
Expectations:
|
|
42
|
+
|
|
43
|
+
- ``to_dict()`` returns a dict containing only JSON-safe values
|
|
44
|
+
(str/int/float/bool/None/list/dict). Where a shared payload shape
|
|
45
|
+
exists in :mod:`dazzle_lib.payloads`, the dict conforms to it.
|
|
46
|
+
- ``from_dict(data)`` is a classmethod constructing an equivalent object.
|
|
47
|
+
- ``SCHEMA_VERSION`` identifies the dict layout so readers can migrate
|
|
48
|
+
old serialized forms. Bump it on any breaking shape change.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
SCHEMA_VERSION: int
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
54
|
+
"""Return a JSON-safe dict representation of this object."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Serializable":
|
|
59
|
+
"""Construct an instance from a dict produced by :meth:`to_dict`."""
|
|
60
|
+
...
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dazzle-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The DazzleLib bedrock: shared Protocols, TypedDict payload schemas, and exception root for the dazzle-* library stack. Stdlib-only by charter.
|
|
5
|
+
Author-email: djdarcy <djdarcy@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/DazzleLib/dazzle-lib
|
|
8
|
+
Project-URL: Repository, https://github.com/DazzleLib/dazzle-lib
|
|
9
|
+
Project-URL: Issues, https://github.com/DazzleLib/dazzle-lib/issues
|
|
10
|
+
Keywords: dazzlelib,protocols,typeddict,serialization,bedrock,stack
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# dazzle-lib
|
|
32
|
+
|
|
33
|
+
**The DazzleLib stack's bedrock: shared Protocols, TypedDict payload schemas, and the exception root.**
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/dazzle-lib/)
|
|
36
|
+
[](https://pypi.org/project/dazzle-lib/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](docs/platform-support.md)
|
|
39
|
+
|
|
40
|
+
Every `dazzle-*` library ([the stack](https://github.com/DazzleLib/.github/blob/main/docs/STACK-MAP.md)) builds on this package: it defines what stack objects can be expected to do (view themselves, serialize themselves) and what shapes cross-layer payloads have. **Types only** -- by charter this package contains no I/O, no path handling, no platform probing, and no behavior, forever.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install dazzle-lib
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What's inside (all of it)
|
|
47
|
+
|
|
48
|
+
| Module | Contents |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `dazzle_lib.protocols` | `Viewable` (`summary()`/`__str__`), `Serializable` (`to_dict`/`from_dict`/`to_json`, `SCHEMA_VERSION`) -- structural `Protocol`s, `runtime_checkable`, nothing is forced to subclass |
|
|
51
|
+
| `dazzle_lib.payloads` | The cross-layer TypedDict schemas: `FileMetadataDict`, `TimestampsDict`, `WindowsMetadataDict`, `UnixMetadataDict`, `LinkTargetDict`, `HashResultDict` -- mirroring what `dazzle-filekit` actually produces |
|
|
52
|
+
| `dazzle_lib.exceptions` | `DazzleError` root + per-domain bases (`PathIdentityError`, `FileOperationError`, `LinkError`, `PreserveError`) |
|
|
53
|
+
| `dazzle_lib.mixins` | `DazzleDataMixin` -- derives `to_json`/`summary`/`__str__` from your `to_dict` |
|
|
54
|
+
|
|
55
|
+
## The idea: the dict is the interface
|
|
56
|
+
|
|
57
|
+
Rich objects in upper layers know how to become plain dicts; lower-layer functions take and return those dicts. The TypedDicts here are the agreed shapes, so a manifest object in `dazzle-preservelib` and a metadata collector in `dazzle-filekit` speak the same payload without sharing a class hierarchy:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from dataclasses import dataclass
|
|
61
|
+
from dazzle_lib import DazzleDataMixin, Serializable, FileMetadataDict
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class TransferResult(DazzleDataMixin):
|
|
65
|
+
SCHEMA_VERSION = 1
|
|
66
|
+
path: str
|
|
67
|
+
metadata: FileMetadataDict
|
|
68
|
+
|
|
69
|
+
def to_dict(self):
|
|
70
|
+
return {"schema_version": self.SCHEMA_VERSION,
|
|
71
|
+
"path": self.path, "metadata": dict(self.metadata)}
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data):
|
|
75
|
+
return cls(path=data["path"], metadata=data["metadata"])
|
|
76
|
+
|
|
77
|
+
result = TransferResult("a.txt", {"mode": 0o644, "size": 10, "timestamps": {}})
|
|
78
|
+
assert isinstance(result, Serializable) # structural -- no subclassing needed
|
|
79
|
+
print(result.summary()) # one-liner for logs
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
And one catchable root for the whole stack:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from dazzle_lib import DazzleError
|
|
86
|
+
try:
|
|
87
|
+
... # any dazzle-* library call
|
|
88
|
+
except DazzleError as e:
|
|
89
|
+
... # caught, whichever layer raised it
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## The charter (enforced by tests)
|
|
93
|
+
|
|
94
|
+
This package is **stdlib-only forever** and contains **no behavior**. `tests/test_charter.py` fails on any banned import (`os`, `shutil`, `pathlib`, `subprocess`, ...) anywhere in the package -- a PR that needs to weaken that test is adding something that belongs in a higher layer. Admission follows the **rule of two**: a Protocol or TypedDict enters the bedrock only when two or more stack libraries need it.
|
|
95
|
+
|
|
96
|
+
## The stack
|
|
97
|
+
|
|
98
|
+
| Layer | Library | Role |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| B | **dazzle-lib** (this) | bedrock contracts |
|
|
101
|
+
| L0 | [dazzle-unctools](https://github.com/DazzleLib/UNCtools) | path identity (UNC/drive/origin) |
|
|
102
|
+
| L1 | [dazzle-filekit](https://github.com/DazzleLib/dazzle-filekit) | filesystem primitives |
|
|
103
|
+
| L2 | dazzle-linklib *(planned)* | link serialization |
|
|
104
|
+
| L3 | dazzle-preservelib *(planned)* | operation orchestration |
|
|
105
|
+
| ⊥ | [dazzle-treelib](https://github.com/DazzleLib/dazzle-tree-lib) | traversal engine |
|
|
106
|
+
|
|
107
|
+
Full architecture contract: [STACK-MAP.md](https://github.com/DazzleLib/.github/blob/main/docs/STACK-MAP.md). API stability policy: [docs/api-stability.md](docs/api-stability.md).
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install -e ".[dev]"
|
|
113
|
+
python -m pytest tests/ -v
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT -- the bedrock sits beneath MIT and GPL stack members alike, so it carries the permissive license.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
dazzle_lib/__init__.py
|
|
5
|
+
dazzle_lib/__main__.py
|
|
6
|
+
dazzle_lib/_version.py
|
|
7
|
+
dazzle_lib/exceptions.py
|
|
8
|
+
dazzle_lib/mixins.py
|
|
9
|
+
dazzle_lib/payloads.py
|
|
10
|
+
dazzle_lib/protocols.py
|
|
11
|
+
dazzle_lib.egg-info/PKG-INFO
|
|
12
|
+
dazzle_lib.egg-info/SOURCES.txt
|
|
13
|
+
dazzle_lib.egg-info/dependency_links.txt
|
|
14
|
+
dazzle_lib.egg-info/requires.txt
|
|
15
|
+
dazzle_lib.egg-info/top_level.txt
|
|
16
|
+
tests/test_charter.py
|
|
17
|
+
tests/test_import_stability.py
|
|
18
|
+
tests/test_protocols_and_mixin.py
|
|
19
|
+
tests/test_version.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dazzle_lib
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dazzle-lib"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "The DazzleLib bedrock: shared Protocols, TypedDict payload schemas, and exception root for the dazzle-* library stack. Stdlib-only by charter."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "djdarcy", email = "djdarcy@users.noreply.github.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["dazzlelib", "protocols", "typeddict", "serialization", "bedrock", "stack"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Libraries",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.0.0",
|
|
36
|
+
"pytest-cov>=4.0.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/DazzleLib/dazzle-lib"
|
|
41
|
+
Repository = "https://github.com/DazzleLib/dazzle-lib"
|
|
42
|
+
Issues = "https://github.com/DazzleLib/dazzle-lib/issues"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.dynamic]
|
|
45
|
+
version = {attr = "dazzle_lib._version.PIP_VERSION"}
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["."]
|
|
49
|
+
include = ["dazzle_lib", "dazzle_lib.*"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
python_files = "test_*.py"
|
|
54
|
+
python_functions = "test_*"
|
|
55
|
+
python_classes = "Test*"
|
|
56
|
+
|
|
57
|
+
[tool.repokit-common]
|
|
58
|
+
version-source = "dazzle_lib/_version.py"
|
|
59
|
+
changelog = "CHANGELOG.md"
|
|
60
|
+
repo-url = "https://github.com/DazzleLib/dazzle-lib"
|
|
61
|
+
tag-prefix = "v"
|
|
62
|
+
private-patterns = ["private/", "local/", ".env"]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""The charter test: dazzle-lib is types-only, stdlib-only, behavior-free.
|
|
2
|
+
|
|
3
|
+
This is the god-library guard from the STACK-MAP (D10). It fails the moment
|
|
4
|
+
anyone adds I/O, path handling, platform probing, or subprocess use to the
|
|
5
|
+
bedrock. A PR that needs to weaken this test is, by definition, adding
|
|
6
|
+
behavior that belongs in a higher layer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import dazzle_lib
|
|
13
|
+
|
|
14
|
+
PACKAGE_DIR = Path(dazzle_lib.__file__).parent
|
|
15
|
+
|
|
16
|
+
# Modules whose import (at any level) means behavior crept in.
|
|
17
|
+
BANNED_IMPORTS = {
|
|
18
|
+
"os", # I/O + platform probing
|
|
19
|
+
"io",
|
|
20
|
+
"shutil",
|
|
21
|
+
"pathlib", # path handling is L1's domain (charter test uses it; the package may not)
|
|
22
|
+
"subprocess",
|
|
23
|
+
"socket",
|
|
24
|
+
"platform",
|
|
25
|
+
"ctypes",
|
|
26
|
+
"tempfile",
|
|
27
|
+
"glob",
|
|
28
|
+
"fnmatch",
|
|
29
|
+
"stat",
|
|
30
|
+
"sys", # no interpreter poking either; types don't need it
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# _version.py is generated/managed by repokit hooks and exempt (it reads nothing).
|
|
34
|
+
EXEMPT_FILES = {"_version.py"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _module_files():
|
|
38
|
+
return [
|
|
39
|
+
p for p in PACKAGE_DIR.glob("*.py")
|
|
40
|
+
if p.name not in EXEMPT_FILES
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _imports_of(path: Path):
|
|
45
|
+
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
46
|
+
found = set()
|
|
47
|
+
for node in ast.walk(tree):
|
|
48
|
+
if isinstance(node, ast.Import):
|
|
49
|
+
for alias in node.names:
|
|
50
|
+
found.add(alias.name.split(".")[0])
|
|
51
|
+
elif isinstance(node, ast.ImportFrom):
|
|
52
|
+
if node.module and node.level == 0:
|
|
53
|
+
found.add(node.module.split(".")[0])
|
|
54
|
+
return found
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_no_banned_imports():
|
|
58
|
+
violations = {}
|
|
59
|
+
for path in _module_files():
|
|
60
|
+
bad = _imports_of(path) & BANNED_IMPORTS
|
|
61
|
+
if bad:
|
|
62
|
+
violations[path.name] = sorted(bad)
|
|
63
|
+
assert not violations, (
|
|
64
|
+
f"CHARTER VIOLATION -- behavior-bearing imports in the bedrock: {violations}. "
|
|
65
|
+
f"dazzle-lib is types-only; this capability belongs in a higher layer."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_stdlib_only():
|
|
70
|
+
"""No third-party imports, ever (the package must not even import filekit)."""
|
|
71
|
+
allowed = {"json", "typing", "enum", "dataclasses", "abc", "collections",
|
|
72
|
+
"datetime", "ast", "dazzle_lib"}
|
|
73
|
+
violations = {}
|
|
74
|
+
for path in _module_files():
|
|
75
|
+
extra = _imports_of(path) - allowed - BANNED_IMPORTS
|
|
76
|
+
if extra:
|
|
77
|
+
violations[path.name] = sorted(extra)
|
|
78
|
+
assert not violations, (
|
|
79
|
+
f"Unexpected imports in the bedrock (stdlib-only, and only the boring "
|
|
80
|
+
f"parts): {violations}. If legitimately needed, add to the allowlist "
|
|
81
|
+
f"in this test WITH a charter justification in the commit message."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_no_filesystem_calls_in_source_text():
|
|
86
|
+
"""Belt-and-braces: no open()/Path( usage even without an import."""
|
|
87
|
+
for path in _module_files():
|
|
88
|
+
text = path.read_text(encoding="utf-8")
|
|
89
|
+
tree = ast.parse(text)
|
|
90
|
+
for node in ast.walk(tree):
|
|
91
|
+
if isinstance(node, ast.Call):
|
|
92
|
+
fn = node.func
|
|
93
|
+
name = getattr(fn, "id", getattr(fn, "attr", ""))
|
|
94
|
+
assert name != "open", (
|
|
95
|
+
f"CHARTER VIOLATION -- open() call in {path.name}"
|
|
96
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Import-stability canary (see docs/api-stability.md).
|
|
2
|
+
|
|
3
|
+
Every symbol listed here is part of the locked public API. If this test
|
|
4
|
+
fails, a consumer somewhere breaks: do NOT silently fix the test -- follow
|
|
5
|
+
the api-stability.md process (deprecate with a noisy shim, register it,
|
|
6
|
+
slate removal).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
|
|
11
|
+
LOCKED_SURFACE = {
|
|
12
|
+
"dazzle_lib": [
|
|
13
|
+
"__version__",
|
|
14
|
+
"__app_name__",
|
|
15
|
+
"Viewable",
|
|
16
|
+
"Serializable",
|
|
17
|
+
"TimestampsDict",
|
|
18
|
+
"WindowsMetadataDict",
|
|
19
|
+
"UnixMetadataDict",
|
|
20
|
+
"FileMetadataDict",
|
|
21
|
+
"LinkTargetDict",
|
|
22
|
+
"HashResultDict",
|
|
23
|
+
"DazzleError",
|
|
24
|
+
"PathIdentityError",
|
|
25
|
+
"FileOperationError",
|
|
26
|
+
"LinkError",
|
|
27
|
+
"PreserveError",
|
|
28
|
+
"DazzleDataMixin",
|
|
29
|
+
],
|
|
30
|
+
"dazzle_lib.protocols": ["Viewable", "Serializable"],
|
|
31
|
+
"dazzle_lib.payloads": [
|
|
32
|
+
"TimestampsDict",
|
|
33
|
+
"WindowsMetadataDict",
|
|
34
|
+
"UnixMetadataDict",
|
|
35
|
+
"FileMetadataDict",
|
|
36
|
+
"LinkTargetDict",
|
|
37
|
+
"HashResultDict",
|
|
38
|
+
],
|
|
39
|
+
"dazzle_lib.exceptions": [
|
|
40
|
+
"DazzleError",
|
|
41
|
+
"PathIdentityError",
|
|
42
|
+
"FileOperationError",
|
|
43
|
+
"LinkError",
|
|
44
|
+
"PreserveError",
|
|
45
|
+
],
|
|
46
|
+
"dazzle_lib.mixins": ["DazzleDataMixin"],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_locked_surface_importable():
|
|
51
|
+
missing = []
|
|
52
|
+
for module_name, symbols in LOCKED_SURFACE.items():
|
|
53
|
+
module = importlib.import_module(module_name)
|
|
54
|
+
for symbol in symbols:
|
|
55
|
+
if not hasattr(module, symbol):
|
|
56
|
+
missing.append(f"{module_name}.{symbol}")
|
|
57
|
+
assert not missing, (
|
|
58
|
+
f"Locked API symbols missing: {missing} -- see docs/api-stability.md "
|
|
59
|
+
f"before changing the public surface."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_exception_hierarchy_rooted():
|
|
64
|
+
from dazzle_lib import (
|
|
65
|
+
DazzleError,
|
|
66
|
+
FileOperationError,
|
|
67
|
+
LinkError,
|
|
68
|
+
PathIdentityError,
|
|
69
|
+
PreserveError,
|
|
70
|
+
)
|
|
71
|
+
for exc in (PathIdentityError, FileOperationError, LinkError, PreserveError):
|
|
72
|
+
assert issubclass(exc, DazzleError)
|
|
73
|
+
assert issubclass(DazzleError, Exception)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Protocol satisfaction + mixin behavior: a plain dataclass with to_dict/
|
|
2
|
+
from_dict/SCHEMA_VERSION satisfies Serializable WITHOUT subclassing anything --
|
|
3
|
+
the structural-typing promise the bedrock makes."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
|
|
9
|
+
from dazzle_lib import DazzleDataMixin, Serializable, Viewable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SampleResult(DazzleDataMixin):
|
|
14
|
+
SCHEMA_VERSION = 1
|
|
15
|
+
name: str = "demo"
|
|
16
|
+
count: int = 3
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
19
|
+
return {"schema_version": self.SCHEMA_VERSION, "name": self.name,
|
|
20
|
+
"count": self.count}
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SampleResult":
|
|
24
|
+
return cls(name=data["name"], count=data["count"])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BareDuck:
|
|
28
|
+
"""No mixin, no inheritance -- pure structural satisfaction."""
|
|
29
|
+
|
|
30
|
+
SCHEMA_VERSION = 2
|
|
31
|
+
|
|
32
|
+
def to_dict(self):
|
|
33
|
+
return {"x": 1}
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, data):
|
|
37
|
+
return cls()
|
|
38
|
+
|
|
39
|
+
def summary(self):
|
|
40
|
+
return "a bare duck"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_dataclass_with_mixin_satisfies_serializable():
|
|
44
|
+
s = SampleResult()
|
|
45
|
+
assert isinstance(s, Serializable)
|
|
46
|
+
assert isinstance(s, Viewable)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_bare_class_satisfies_protocols_structurally():
|
|
50
|
+
d = BareDuck()
|
|
51
|
+
assert isinstance(d, Serializable)
|
|
52
|
+
assert isinstance(d, Viewable)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_non_conforming_object_rejected():
|
|
56
|
+
assert not isinstance(object(), Serializable)
|
|
57
|
+
assert not isinstance(object(), Viewable)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_round_trip():
|
|
61
|
+
s = SampleResult(name="rt", count=7)
|
|
62
|
+
again = SampleResult.from_dict(s.to_dict())
|
|
63
|
+
assert again == s
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_mixin_to_json_and_str():
|
|
67
|
+
s = SampleResult(name="js", count=1)
|
|
68
|
+
parsed = json.loads(s.to_json())
|
|
69
|
+
assert parsed == {"schema_version": 1, "name": "js", "count": 1}
|
|
70
|
+
assert json.loads(str(s)) == parsed
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_mixin_summary_is_one_line():
|
|
74
|
+
s = SampleResult()
|
|
75
|
+
assert "\n" not in s.summary()
|
|
76
|
+
assert "SampleResult" in s.summary()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_payload_typeddicts_are_constructible():
|
|
80
|
+
from dazzle_lib import FileMetadataDict, TimestampsDict
|
|
81
|
+
|
|
82
|
+
ts: TimestampsDict = {"modified": 1.0, "accessed": 2.0, "created": 3.0}
|
|
83
|
+
md: FileMetadataDict = {"mode": 0o644, "size": 10, "timestamps": ts}
|
|
84
|
+
assert md["timestamps"]["modified"] == 1.0
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Tests for version module."""
|
|
2
|
+
|
|
3
|
+
from dazzle_lib._version import (
|
|
4
|
+
MAJOR, MINOR, PATCH, PHASE, PROJECT_PHASE,
|
|
5
|
+
get_version, get_base_version, get_display_version, get_pip_version,
|
|
6
|
+
__app_name__,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_app_name():
|
|
11
|
+
assert __app_name__ == "dazzle-lib"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_version_components():
|
|
15
|
+
assert isinstance(MAJOR, int)
|
|
16
|
+
assert isinstance(MINOR, int)
|
|
17
|
+
assert isinstance(PATCH, int)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_phase_valid():
|
|
21
|
+
"""PHASE is empty string (stable release) or a string like 'alpha', 'beta', 'rc1'."""
|
|
22
|
+
assert isinstance(PHASE, str)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_get_version_returns_string():
|
|
26
|
+
v = get_version()
|
|
27
|
+
assert isinstance(v, str)
|
|
28
|
+
assert len(v) > 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_base_version_format():
|
|
32
|
+
base = get_base_version()
|
|
33
|
+
assert base.startswith(f"{MAJOR}.{MINOR}.{PATCH}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_display_version_includes_project_phase():
|
|
37
|
+
display = get_display_version()
|
|
38
|
+
if PROJECT_PHASE and PROJECT_PHASE != "stable":
|
|
39
|
+
assert PROJECT_PHASE.upper() in display
|
|
40
|
+
else:
|
|
41
|
+
assert display == get_base_version()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_pip_version_pep440():
|
|
45
|
+
pip_v = get_pip_version()
|
|
46
|
+
assert "-" not in pip_v
|
|
47
|
+
if PHASE:
|
|
48
|
+
assert any(c.isalpha() for c in pip_v.split(".")[-1])
|
|
49
|
+
else:
|
|
50
|
+
assert all(c.isdigit() or c == "." for c in pip_v)
|