pafex 2.3__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.
- pafex-2.3/LICENSE +21 -0
- pafex-2.3/PKG-INFO +55 -0
- pafex-2.3/README.md +42 -0
- pafex-2.3/pafex/__init__.py +29 -0
- pafex-2.3/pafex/config.py +255 -0
- pafex-2.3/pafex/exceptions.py +17 -0
- pafex-2.3/pafex.egg-info/PKG-INFO +55 -0
- pafex-2.3/pafex.egg-info/SOURCES.txt +10 -0
- pafex-2.3/pafex.egg-info/dependency_links.txt +1 -0
- pafex-2.3/pafex.egg-info/top_level.txt +1 -0
- pafex-2.3/pyproject.toml +21 -0
- pafex-2.3/setup.cfg +4 -0
pafex-2.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 pafex
|
|
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.
|
pafex-2.3/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pafex
|
|
3
|
+
Version: 2.3
|
|
4
|
+
Summary: Minimal checksummed binary configuration handling.
|
|
5
|
+
Author-email: redomaitic <redomaitic@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
Minimal checksummed binary configuration handling for Python. `Config`
|
|
15
|
+
layers user settings over `DEFAULT_CONFIG`; `load_config`/`save_config`
|
|
16
|
+
persist to a strict binary format with optional gzip/xz/zlib compression.
|
|
17
|
+
Standard library only.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install pafex
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from pafex import Config, load_config, save_config
|
|
29
|
+
|
|
30
|
+
cfg = Config({'strict_mode': True, 'allowed_tags': {'p', 'a'}})
|
|
31
|
+
cfg.get('strict_mode') # True
|
|
32
|
+
cfg.get('strip_whitespace') # default from DEFAULT_CONFIG
|
|
33
|
+
cfg.update({'max_depth': 3}) # new Config, original untouched
|
|
34
|
+
|
|
35
|
+
save_config(cfg, 'default.config', compression='gzip')
|
|
36
|
+
loaded = load_config('default.config')
|
|
37
|
+
loaded == cfg # True
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Config file layout:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
raw: [8-byte MD5 checksum][4-byte size][config]
|
|
44
|
+
gzip: [8-byte MD5 checksum][4-byte size][gzip.compress(config)]
|
|
45
|
+
xz: [8-byte MD5 checksum][4-byte size][lzma.compress(config)]
|
|
46
|
+
zlib: [8-byte MD5 checksum][4-byte size][zlib.compress(config)]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Writes are atomic (temp file + `os.replace`). With no explicit
|
|
50
|
+
path, `load_config`/`save_config` locate the caller's `default.config`
|
|
51
|
+
(caller's module directory first, then the package directory).
|
|
52
|
+
|
|
53
|
+
Errors raise `PafexError` subclasses: `ConfigLoadError` (read/parse
|
|
54
|
+
failures), `ConfigChecksumError` (integrity mismatch) and
|
|
55
|
+
`ConfigDecompressError` (corrupt compressed payload).
|
pafex-2.3/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Minimal checksummed binary configuration handling for Python. `Config`
|
|
2
|
+
layers user settings over `DEFAULT_CONFIG`; `load_config`/`save_config`
|
|
3
|
+
persist to a strict binary format with optional gzip/xz/zlib compression.
|
|
4
|
+
Standard library only.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install pafex
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from pafex import Config, load_config, save_config
|
|
16
|
+
|
|
17
|
+
cfg = Config({'strict_mode': True, 'allowed_tags': {'p', 'a'}})
|
|
18
|
+
cfg.get('strict_mode') # True
|
|
19
|
+
cfg.get('strip_whitespace') # default from DEFAULT_CONFIG
|
|
20
|
+
cfg.update({'max_depth': 3}) # new Config, original untouched
|
|
21
|
+
|
|
22
|
+
save_config(cfg, 'default.config', compression='gzip')
|
|
23
|
+
loaded = load_config('default.config')
|
|
24
|
+
loaded == cfg # True
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Config file layout:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
raw: [8-byte MD5 checksum][4-byte size][config]
|
|
31
|
+
gzip: [8-byte MD5 checksum][4-byte size][gzip.compress(config)]
|
|
32
|
+
xz: [8-byte MD5 checksum][4-byte size][lzma.compress(config)]
|
|
33
|
+
zlib: [8-byte MD5 checksum][4-byte size][zlib.compress(config)]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Writes are atomic (temp file + `os.replace`). With no explicit
|
|
37
|
+
path, `load_config`/`save_config` locate the caller's `default.config`
|
|
38
|
+
(caller's module directory first, then the package directory).
|
|
39
|
+
|
|
40
|
+
Errors raise `PafexError` subclasses: `ConfigLoadError` (read/parse
|
|
41
|
+
failures), `ConfigChecksumError` (integrity mismatch) and
|
|
42
|
+
`ConfigDecompressError` (corrupt compressed payload).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""pafex: minimal checksummed binary configuration handling."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
ConfigChecksumError,
|
|
5
|
+
ConfigDecompressError,
|
|
6
|
+
ConfigLoadError,
|
|
7
|
+
PafexError,
|
|
8
|
+
)
|
|
9
|
+
from .config import (
|
|
10
|
+
CONFIG_NAME,
|
|
11
|
+
DEFAULT_CONFIG,
|
|
12
|
+
Config,
|
|
13
|
+
find_default_config,
|
|
14
|
+
load_config,
|
|
15
|
+
save_config,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'Config',
|
|
20
|
+
'DEFAULT_CONFIG',
|
|
21
|
+
'CONFIG_NAME',
|
|
22
|
+
'load_config',
|
|
23
|
+
'save_config',
|
|
24
|
+
'find_default_config',
|
|
25
|
+
'PafexError',
|
|
26
|
+
'ConfigLoadError',
|
|
27
|
+
'ConfigChecksumError',
|
|
28
|
+
'ConfigDecompressError',
|
|
29
|
+
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Configuration loading and persistence for the pafex package.
|
|
2
|
+
|
|
3
|
+
Config files use a strict binary layout:
|
|
4
|
+
|
|
5
|
+
raw: [8-byte MD5 checksum][4-byte size][payload]
|
|
6
|
+
gzip: [8-byte MD5 checksum][4-byte size][gzip.compress(payload)]
|
|
7
|
+
xz: [8-byte MD5 checksum][4-byte size][lzma.compress(payload)]
|
|
8
|
+
zlib: [8-byte MD5 checksum][4-byte size][zlib.compress(payload)]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import gzip
|
|
12
|
+
import hashlib
|
|
13
|
+
import importlib.util
|
|
14
|
+
import lzma
|
|
15
|
+
import os
|
|
16
|
+
import pickle
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import types
|
|
20
|
+
import zlib
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, Mapping, Optional, Tuple, Union
|
|
23
|
+
|
|
24
|
+
from .exceptions import ConfigChecksumError, ConfigDecompressError, ConfigLoadError
|
|
25
|
+
|
|
26
|
+
PathLike = Union[str, os.PathLike]
|
|
27
|
+
|
|
28
|
+
DEFAULT_CONFIG: Mapping[str, Any] = types.MappingProxyType({
|
|
29
|
+
'parse_tables': True,
|
|
30
|
+
'strip_whitespace': True,
|
|
31
|
+
'encoding': 'utf-8',
|
|
32
|
+
'max_depth': 100,
|
|
33
|
+
'default_tag': 'div',
|
|
34
|
+
'preserve_comments': False,
|
|
35
|
+
'strict_mode': False,
|
|
36
|
+
'remove_comments': True,
|
|
37
|
+
'allowed_tags': frozenset({'p', 'br', 'b', 'i', 'a', 'ul', 'ol', 'li', 'div', 'span'}),
|
|
38
|
+
'max_file_size_mb': 10,
|
|
39
|
+
'cache_size': 50,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
_CHECKSUM_SIZE = 8
|
|
43
|
+
_SIZE_SIZE = 4
|
|
44
|
+
_HEADER_SIZE = _CHECKSUM_SIZE + _SIZE_SIZE
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _checksum(payload: bytes) -> bytes:
|
|
48
|
+
"""First _CHECKSUM_SIZE bytes of the MD5 digest of payload."""
|
|
49
|
+
return hashlib.md5(payload).digest()[:_CHECKSUM_SIZE]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _detect_compression(data: bytes) -> Optional[Tuple[str, Any]]:
|
|
53
|
+
"""Detect compression format by magic bytes. Returns (name, module) or None.
|
|
54
|
+
|
|
55
|
+
zlib has no magic number; it is detected last by validating its CMF/FLG
|
|
56
|
+
header (deflate method, CINFO <= 7, no preset dictionary, header value
|
|
57
|
+
divisible by 31).
|
|
58
|
+
"""
|
|
59
|
+
if data[:2] == b'\x1f\x8b':
|
|
60
|
+
return 'gzip', gzip
|
|
61
|
+
if data[:6] == b'\xfd7zXZ\x00':
|
|
62
|
+
return 'xz', lzma
|
|
63
|
+
if (
|
|
64
|
+
len(data) >= 2
|
|
65
|
+
and data[0] & 0x0F == 8
|
|
66
|
+
and data[0] >> 4 <= 7
|
|
67
|
+
and not data[1] & 0x20
|
|
68
|
+
and ((data[0] << 8) | data[1]) % 31 == 0
|
|
69
|
+
):
|
|
70
|
+
return 'zlib', zlib
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _verify_header(data: bytes, path: PathLike) -> bytes:
|
|
75
|
+
"""Verify the 8-byte MD5 and 4-byte size header; return the payload."""
|
|
76
|
+
if len(data) < _HEADER_SIZE:
|
|
77
|
+
raise ConfigLoadError(
|
|
78
|
+
f"config file '{path}' is too short to be a valid config file"
|
|
79
|
+
)
|
|
80
|
+
stored = data[:_CHECKSUM_SIZE]
|
|
81
|
+
size = int.from_bytes(data[_CHECKSUM_SIZE:_HEADER_SIZE], 'big')
|
|
82
|
+
payload = data[_HEADER_SIZE:]
|
|
83
|
+
if len(payload) != size:
|
|
84
|
+
raise ConfigLoadError(
|
|
85
|
+
f"size mismatch in config file '{path}': file truncated or "
|
|
86
|
+
f"tampered with; expected {size} payload bytes, got {len(payload)}"
|
|
87
|
+
)
|
|
88
|
+
if _checksum(payload) != stored:
|
|
89
|
+
raise ConfigChecksumError(
|
|
90
|
+
f"checksum mismatch in config file '{path}': file may be corrupted "
|
|
91
|
+
"or tampered with; files written by older versions are not accepted"
|
|
92
|
+
)
|
|
93
|
+
return payload
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _sanitize_value(value: Any) -> Any:
|
|
97
|
+
"""Recursively verify that a value only contains allowed plain types."""
|
|
98
|
+
if isinstance(value, (str, bytes, int, float, bool, type(None))):
|
|
99
|
+
return value
|
|
100
|
+
if isinstance(value, dict):
|
|
101
|
+
return {str(key): _sanitize_value(item) for key, item in value.items()}
|
|
102
|
+
if isinstance(value, (list, tuple)):
|
|
103
|
+
return [_sanitize_value(item) for item in value]
|
|
104
|
+
if isinstance(value, (set, frozenset)):
|
|
105
|
+
return {_sanitize_value(item) for item in value}
|
|
106
|
+
raise ConfigLoadError(
|
|
107
|
+
f'disallowed type {type(value).__name__!r} in config file; '
|
|
108
|
+
'only plain data types are allowed'
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Config:
|
|
113
|
+
"""Configuration holder that layers user settings over DEFAULT_CONFIG."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, settings: Optional[Dict[str, Any]] = None):
|
|
116
|
+
self._settings: Dict[str, Any] = {**DEFAULT_CONFIG, **(settings or {})}
|
|
117
|
+
|
|
118
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
119
|
+
return self._settings.get(key, default)
|
|
120
|
+
|
|
121
|
+
def update(self, settings: Dict[str, Any]) -> 'Config':
|
|
122
|
+
"""Return a new Config with extra settings layered on top of this one."""
|
|
123
|
+
return Config({**self._settings, **(settings or {})})
|
|
124
|
+
|
|
125
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
126
|
+
return dict(self._settings)
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def load_default(cls) -> 'Config':
|
|
130
|
+
"""Return a Config built from DEFAULT_CONFIG without reading any file."""
|
|
131
|
+
return cls()
|
|
132
|
+
|
|
133
|
+
def __eq__(self, other: Any) -> bool:
|
|
134
|
+
if isinstance(other, Config):
|
|
135
|
+
return self._settings == other._settings
|
|
136
|
+
return NotImplemented
|
|
137
|
+
|
|
138
|
+
def __repr__(self) -> str:
|
|
139
|
+
return f'Config({self._settings})'
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _read_config(path: PathLike) -> Dict[str, Any]:
|
|
143
|
+
"""Read config bytes from path."""
|
|
144
|
+
try:
|
|
145
|
+
with open(path, 'rb') as handle:
|
|
146
|
+
data = handle.read()
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
raise ConfigLoadError(f"failed to read config file '{path}': {exc}") from exc
|
|
149
|
+
|
|
150
|
+
blob = _verify_header(data, path)
|
|
151
|
+
|
|
152
|
+
detected = _detect_compression(blob)
|
|
153
|
+
if detected is not None:
|
|
154
|
+
name, module = detected
|
|
155
|
+
try:
|
|
156
|
+
blob = module.decompress(blob)
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
raise ConfigDecompressError(
|
|
159
|
+
f"failed to decompress {name} config file '{path}': {exc}"
|
|
160
|
+
) from exc
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
loaded = pickle.loads(blob)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
raise ConfigLoadError(
|
|
166
|
+
f"failed to load pickle payload from '{path}': {exc}"
|
|
167
|
+
) from exc
|
|
168
|
+
if not isinstance(loaded, dict):
|
|
169
|
+
raise ConfigLoadError(
|
|
170
|
+
f"config file '{path}' must contain a dict, "
|
|
171
|
+
f"got {type(loaded).__name__}"
|
|
172
|
+
)
|
|
173
|
+
return _sanitize_value(loaded)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def load_config(path: Optional[PathLike] = None) -> Config:
|
|
177
|
+
"""Load a Config from an explicit file, or the caller's default.config."""
|
|
178
|
+
if path is None:
|
|
179
|
+
path = _locate_config(sys._getframe(1).f_globals)
|
|
180
|
+
return Config(_read_config(path))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def save_config(
|
|
184
|
+
config: Union[Config, Dict[str, Any]],
|
|
185
|
+
path: Optional[PathLike] = None,
|
|
186
|
+
compression: Optional[str] = None,
|
|
187
|
+
) -> Path:
|
|
188
|
+
"""Persist settings to path atomically; no path auto-finds default.config."""
|
|
189
|
+
if compression not in (None, 'gzip', 'xz', 'zlib'):
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"unsupported compression {compression!r}; expected 'gzip', 'xz', 'zlib' or None"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if path is None:
|
|
195
|
+
path = _locate_config(sys._getframe(1).f_globals)
|
|
196
|
+
|
|
197
|
+
if isinstance(config, Config):
|
|
198
|
+
settings = config.to_dict()
|
|
199
|
+
else:
|
|
200
|
+
settings = Config(config).to_dict()
|
|
201
|
+
payload = pickle.dumps(settings, protocol=pickle.HIGHEST_PROTOCOL)
|
|
202
|
+
if compression == 'gzip':
|
|
203
|
+
payload = gzip.compress(payload)
|
|
204
|
+
elif compression == 'xz':
|
|
205
|
+
payload = lzma.compress(payload)
|
|
206
|
+
elif compression == 'zlib':
|
|
207
|
+
payload = zlib.compress(payload)
|
|
208
|
+
blob = _checksum(payload) + len(payload).to_bytes(_SIZE_SIZE, 'big') + payload
|
|
209
|
+
|
|
210
|
+
target = Path(path)
|
|
211
|
+
if not target.parent.exists():
|
|
212
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
|
|
214
|
+
fd, tmp_name = tempfile.mkstemp(dir=str(target.parent), suffix='.config.tmp')
|
|
215
|
+
try:
|
|
216
|
+
with os.fdopen(fd, 'wb') as handle:
|
|
217
|
+
handle.write(blob)
|
|
218
|
+
os.replace(tmp_name, str(target))
|
|
219
|
+
except Exception:
|
|
220
|
+
try:
|
|
221
|
+
os.unlink(tmp_name)
|
|
222
|
+
except OSError:
|
|
223
|
+
pass
|
|
224
|
+
raise
|
|
225
|
+
return target
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
CONFIG_NAME = 'default.config'
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _locate_config(g: Dict[str, Any]) -> Path:
|
|
232
|
+
"""Resolve the default.config path for a module globals mapping."""
|
|
233
|
+
file = g.get('__file__')
|
|
234
|
+
if file:
|
|
235
|
+
candidate = Path(os.path.abspath(file)).parent / CONFIG_NAME
|
|
236
|
+
if candidate.exists():
|
|
237
|
+
return candidate
|
|
238
|
+
package = g.get('__package__') or g.get('__name__', '').partition('.')[0]
|
|
239
|
+
if package:
|
|
240
|
+
try:
|
|
241
|
+
spec = importlib.util.find_spec(package)
|
|
242
|
+
except (ImportError, AttributeError, ValueError):
|
|
243
|
+
spec = None
|
|
244
|
+
if spec and spec.submodule_search_locations:
|
|
245
|
+
return Path(spec.submodule_search_locations[0]) / CONFIG_NAME
|
|
246
|
+
raise ConfigLoadError(f'no {CONFIG_NAME} found for caller')
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def find_default_config() -> Path:
|
|
250
|
+
"""Locate the caller's default.config without loading it.
|
|
251
|
+
|
|
252
|
+
Searches the caller's module directory first, then the caller's
|
|
253
|
+
package directory. Raises ConfigLoadError when neither can be resolved.
|
|
254
|
+
"""
|
|
255
|
+
return _locate_config(sys._getframe(1).f_globals)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Custom exceptions for pafex configuration."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PafexError(Exception):
|
|
5
|
+
"""Base exception for all pafex errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigLoadError(PafexError):
|
|
9
|
+
"""Raised when configuration cannot be loaded."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigChecksumError(ConfigLoadError):
|
|
13
|
+
"""Raised when a config file's checksum header does not match its payload."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfigDecompressError(ConfigLoadError):
|
|
17
|
+
"""Raised when a compressed config file cannot be decompressed."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pafex
|
|
3
|
+
Version: 2.3
|
|
4
|
+
Summary: Minimal checksummed binary configuration handling.
|
|
5
|
+
Author-email: redomaitic <redomaitic@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
Minimal checksummed binary configuration handling for Python. `Config`
|
|
15
|
+
layers user settings over `DEFAULT_CONFIG`; `load_config`/`save_config`
|
|
16
|
+
persist to a strict binary format with optional gzip/xz/zlib compression.
|
|
17
|
+
Standard library only.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install pafex
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from pafex import Config, load_config, save_config
|
|
29
|
+
|
|
30
|
+
cfg = Config({'strict_mode': True, 'allowed_tags': {'p', 'a'}})
|
|
31
|
+
cfg.get('strict_mode') # True
|
|
32
|
+
cfg.get('strip_whitespace') # default from DEFAULT_CONFIG
|
|
33
|
+
cfg.update({'max_depth': 3}) # new Config, original untouched
|
|
34
|
+
|
|
35
|
+
save_config(cfg, 'default.config', compression='gzip')
|
|
36
|
+
loaded = load_config('default.config')
|
|
37
|
+
loaded == cfg # True
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Config file layout:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
raw: [8-byte MD5 checksum][4-byte size][config]
|
|
44
|
+
gzip: [8-byte MD5 checksum][4-byte size][gzip.compress(config)]
|
|
45
|
+
xz: [8-byte MD5 checksum][4-byte size][lzma.compress(config)]
|
|
46
|
+
zlib: [8-byte MD5 checksum][4-byte size][zlib.compress(config)]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Writes are atomic (temp file + `os.replace`). With no explicit
|
|
50
|
+
path, `load_config`/`save_config` locate the caller's `default.config`
|
|
51
|
+
(caller's module directory first, then the package directory).
|
|
52
|
+
|
|
53
|
+
Errors raise `PafexError` subclasses: `ConfigLoadError` (read/parse
|
|
54
|
+
failures), `ConfigChecksumError` (integrity mismatch) and
|
|
55
|
+
`ConfigDecompressError` (corrupt compressed payload).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pafex
|
pafex-2.3/pyproject.toml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.1.1", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pafex"
|
|
7
|
+
version = "2.3"
|
|
8
|
+
description = "Minimal checksummed binary configuration handling."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "redomaitic", email = "redomaitic@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
packages = ["pafex"]
|
pafex-2.3/setup.cfg
ADDED