PyperCache 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,44 @@
1
+ """Lightweight wall-clock profiler."""
2
+
3
+ import time
4
+ from typing import Dict
5
+
6
+
7
+ class Profiler:
8
+ """Lightweight label-based wall-clock profiler.
9
+
10
+ Usage::
11
+
12
+ profiler = Profiler()
13
+ profiler.start_profile("my_task")
14
+ do_work()
15
+ profiler.end_profile("my_task") # prints elapsed time if > 1 ms
16
+ """
17
+
18
+ def __init__(self) -> None:
19
+ self.start_times: Dict[str, float] = {}
20
+
21
+ def start_profile(self, label: str) -> None:
22
+ """Record the start time for *label*.
23
+
24
+ Args:
25
+ label: An arbitrary string identifying the profiling session.
26
+ """
27
+ self.start_times[label] = time.time()
28
+
29
+ def end_profile(self, label: str) -> None:
30
+ """End the session for *label* and print the elapsed time.
31
+
32
+ Elapsed times below 1 ms are suppressed to reduce noise. If no
33
+ matching ``start_profile`` call exists a warning is printed instead.
34
+
35
+ Args:
36
+ label: The label passed to the corresponding ``start_profile`` call.
37
+ """
38
+ if label not in self.start_times:
39
+ print(f"No profiling session found for label '{label}'.")
40
+ return
41
+
42
+ elapsed = time.time() - self.start_times[label]
43
+ if elapsed > 0.001:
44
+ print(f"Time elapsed for '{label}': {elapsed:.4f} seconds")
@@ -0,0 +1,26 @@
1
+ """Shared sentinel value for distinguishing "no argument supplied" from None.
2
+
3
+ A single definition lives here so every module in the package uses the same
4
+ object identity, making ``is UNSET`` checks reliable across module boundaries.
5
+ """
6
+
7
+
8
+ class _UnsetType:
9
+ """Singleton sentinel type. Use the ``UNSET`` module-level instance."""
10
+
11
+ _instance: "_UnsetType | None" = None
12
+
13
+ def __new__(cls) -> "_UnsetType":
14
+ if cls._instance is None:
15
+ cls._instance = super().__new__(cls)
16
+ return cls._instance
17
+
18
+ def __repr__(self) -> str:
19
+ return "UNSET"
20
+
21
+ def __bool__(self) -> bool:
22
+ return False
23
+
24
+
25
+ #: The canonical sentinel instance. Test with ``value is UNSET``.
26
+ UNSET = _UnsetType()
@@ -0,0 +1,175 @@
1
+ """Pickle-based persistence and zlib-backed dict compression."""
2
+
3
+ import concurrent.futures
4
+ import json
5
+ import pickle
6
+ import zlib
7
+ from pathlib import Path
8
+ from typing import Any, Dict
9
+
10
+ from PyperCache.utils.fs import ensure_dirs_exist
11
+
12
+
13
+ class PickleStore:
14
+ """Static helpers for persisting arbitrary Python objects via ``pickle``."""
15
+
16
+ @staticmethod
17
+ def touch_file(default_obj: Any, filename: str) -> None:
18
+ """Ensure *filename* exists, creating it with *default_obj* if absent.
19
+
20
+ Args:
21
+ default_obj: The object to pickle if the file does not yet exist.
22
+ filename: Path to the target pickle file.
23
+ """
24
+ if not Path(filename).exists():
25
+ PickleStore.save_object(default_obj, filename)
26
+
27
+ @staticmethod
28
+ def save_object(obj: Any, filename: str) -> None:
29
+ """Serialise *obj* to *filename* using pickle.
30
+
31
+ Intermediate directories are created automatically.
32
+
33
+ Args:
34
+ obj: The Python object to serialise.
35
+ filename: Destination file path.
36
+ """
37
+ try:
38
+ ensure_dirs_exist(filename)
39
+ with open(filename, "wb") as fh:
40
+ pickle.dump(obj, fh)
41
+ except Exception as exc:
42
+ print(f"Failed to save object to {filename}: {exc}")
43
+
44
+ @staticmethod
45
+ def load_object(filename: str) -> Any | None:
46
+ """Deserialise and return the object stored in *filename*.
47
+
48
+ Args:
49
+ filename: Path to a pickle file created by :meth:`save_object`.
50
+
51
+ Returns:
52
+ The deserialised object, or ``None`` if the file is missing or
53
+ an error occurs.
54
+ """
55
+ try:
56
+ with open(filename, "rb") as fh:
57
+ return pickle.load(fh)
58
+ except FileNotFoundError:
59
+ print(f"No such file: {filename}")
60
+ except Exception as exc:
61
+ print(f"Failed to load object from {filename}: {exc}")
62
+ return None
63
+
64
+
65
+ class DataSerializer:
66
+ """Compress, encode, and JSON-serialise dictionaries whose values are
67
+ strings or nested dicts.
68
+
69
+ Compression uses zlib; the compressed bytes are hex-encoded so they can
70
+ be safely embedded in JSON. Serialisation and deserialisation of
71
+ individual keys are parallelised with a ``ThreadPoolExecutor`` —
72
+ ``zlib.compress`` releases the GIL, so threads achieve true parallelism
73
+ here without the IPC overhead of a process pool.
74
+
75
+ Supported value types: ``str``, ``dict``.
76
+ """
77
+
78
+ @staticmethod
79
+ def compress_text(text: str, level: int = 6) -> str:
80
+ """Compress *text* with zlib and return the result as a hex string.
81
+
82
+ Args:
83
+ text: UTF-8 text to compress.
84
+ level: zlib compression level (0–9). Default is 6.
85
+
86
+ Returns:
87
+ Hex-encoded compressed bytes.
88
+ """
89
+ compressed = zlib.compress(text.encode("utf-8"), level)
90
+ return compressed.hex()
91
+
92
+ @staticmethod
93
+ def decompress_text(hex_encoded: str) -> str:
94
+ """Decompress a hex string produced by :meth:`compress_text`.
95
+
96
+ Args:
97
+ hex_encoded: Hex-encoded compressed bytes.
98
+
99
+ Returns:
100
+ The original UTF-8 text.
101
+ """
102
+ compressed = bytes.fromhex(hex_encoded)
103
+ return zlib.decompress(compressed).decode("utf-8")
104
+
105
+ @staticmethod
106
+ def serialize_dict(data: Dict[str, Any], level: int = 6) -> str:
107
+ """Compress each value in *data* and serialise the result to a JSON string.
108
+
109
+ Each value is replaced by a ``(type_tag, compressed_hex)`` tuple so
110
+ that the correct deserialisation path can be chosen later.
111
+
112
+ Args:
113
+ data: A flat dictionary whose values are ``str`` or ``dict``.
114
+ level: zlib compression level passed to :meth:`compress_text`.
115
+
116
+ Returns:
117
+ A JSON string representing the compressed dictionary.
118
+
119
+ Raises:
120
+ ValueError: If a value's type is not ``str`` or ``dict``.
121
+ """
122
+ def compress_entry(key: str, value: Any) -> tuple[str, Any]:
123
+ if isinstance(value, str):
124
+ return key, ("str", DataSerializer.compress_text(value, level))
125
+ elif isinstance(value, dict):
126
+ return key, ("dict", DataSerializer.compress_text(json.dumps(value), level))
127
+ else:
128
+ raise ValueError(f"Serialisation not supported for type {type(value)}.")
129
+
130
+ compressed: Dict[str, Any] = {}
131
+ with concurrent.futures.ThreadPoolExecutor() as executor:
132
+ futures = {
133
+ executor.submit(compress_entry, key, value): key
134
+ for key, value in data.items()
135
+ }
136
+ for future in concurrent.futures.as_completed(futures):
137
+ key, result = future.result()
138
+ compressed[key] = result
139
+
140
+ return json.dumps(compressed)
141
+
142
+ @staticmethod
143
+ def deserialize_dict(json_str: str) -> Dict[str, Any]:
144
+ """Deserialise a JSON string produced by :meth:`serialize_dict`.
145
+
146
+ Each ``(type_tag, compressed_hex)`` pair is decompressed and
147
+ converted back to its original type.
148
+
149
+ Args:
150
+ json_str: A JSON string as returned by :meth:`serialize_dict`.
151
+
152
+ Returns:
153
+ The reconstructed dictionary with original value types restored.
154
+ """
155
+ data: Dict[str, Any] = json.loads(json_str)
156
+
157
+ def decompress_entry(key: str, value: Any) -> tuple[str, Any]:
158
+ type_tag, compressed_hex = value
159
+ if type_tag == "str":
160
+ return key, DataSerializer.decompress_text(compressed_hex)
161
+ elif type_tag == "dict":
162
+ return key, json.loads(DataSerializer.decompress_text(compressed_hex))
163
+ raise ValueError(f"Unknown type tag: {type_tag!r}")
164
+
165
+ result: Dict[str, Any] = {}
166
+ with concurrent.futures.ThreadPoolExecutor() as executor:
167
+ futures = {
168
+ executor.submit(decompress_entry, key, value): key
169
+ for key, value in data.items()
170
+ }
171
+ for future in concurrent.futures.as_completed(futures):
172
+ key, decompressed = future.result()
173
+ result[key] = decompressed
174
+
175
+ return result
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import typing
5
+ from typing import Any
6
+
7
+
8
+ def _is_generic_alias(obj: Any) -> bool:
9
+ try:
10
+ from typing import get_origin
11
+
12
+ return get_origin(obj) is not None
13
+ except Exception:
14
+ return hasattr(obj, "__origin__") and hasattr(obj, "__args__")
15
+
16
+
17
+ def instantiate_type(target_type: Any, data: Any) -> Any:
18
+ """Instantiate or cast *data* into *target_type*.
19
+
20
+ Supports simple classes (preferring ``from_dict``), dataclasses, and
21
+ basic generics: ``list[T]`` and ``dict[K, V]``. Falls back to returning
22
+ the original *data* when no casting is possible.
23
+ """
24
+ if data is None:
25
+ return None
26
+
27
+ # Generic aliases (e.g., list[User], typing.List[User])
28
+ origin = None
29
+ args = ()
30
+ try:
31
+ origin = typing.get_origin(target_type)
32
+ args = typing.get_args(target_type)
33
+ except Exception:
34
+ pass
35
+
36
+ if origin is list or origin is typing.List:
37
+ item_type = args[0] if args else Any
38
+ return [instantiate_type(item_type, item) for item in (data or [])]
39
+
40
+ if origin is dict or origin is typing.Dict:
41
+ key_type, val_type = (args + (Any, Any))[:2]
42
+ return {k: instantiate_type(val_type, v) for k, v in (data or {}).items()}
43
+
44
+ # If a typing alias without origin (fallback), try basic handling
45
+ if _is_generic_alias(target_type):
46
+ # best-effort for simple single-arg generics
47
+ try:
48
+ inner = target_type.__args__[0]
49
+ if target_type.__origin__ is list:
50
+ return [instantiate_type(inner, item) for item in (data or [])]
51
+ except Exception:
52
+ pass
53
+
54
+ # Concrete class handling
55
+ if isinstance(target_type, type):
56
+ # Prefer explicit from_dict constructor
57
+ if hasattr(target_type, "from_dict") and callable(getattr(target_type, "from_dict")):
58
+ return target_type.from_dict(data)
59
+
60
+ # dataclass support
61
+ if dataclasses.is_dataclass(target_type):
62
+ if isinstance(data, dict):
63
+ return target_type(**data)
64
+
65
+ # Last resort: try calling the type with the data as positional arg
66
+ try:
67
+ return target_type(data)
68
+ except Exception:
69
+ return data
70
+
71
+ # Not a class or supported generic — return as-is.
72
+ return data
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyperCache
3
+ Version: 0.1.0
4
+ Summary: Durable file-backed caching for JSON-like data with pluggable storage backends
5
+ Author-email: Brandon Bahret <your.email@example.com>
6
+ Maintainer-email: Brandon Bahret <your.email@example.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 Brandon Bahret
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Project-URL: Homepage, https://github.com/BrandonBahret/PyperCache
29
+ Project-URL: Documentation, https://github.com/BrandonBahret/PyperCache#readme
30
+ Project-URL: Repository, https://github.com/BrandonBahret/PyperCache
31
+ Project-URL: Issues, https://github.com/BrandonBahret/PyperCache/issues
32
+ Project-URL: Changelog, https://github.com/BrandonBahret/PyperCache/blob/master/CHANGELOG.md
33
+ Keywords: cache,persistence,json,pickle,sqlite,storage
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.8
40
+ Classifier: Programming Language :: Python :: 3.9
41
+ Classifier: Programming Language :: Python :: 3.10
42
+ Classifier: Programming Language :: Python :: 3.11
43
+ Classifier: Programming Language :: Python :: 3.12
44
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
45
+ Classifier: Topic :: System :: Distributed Computing
46
+ Requires-Python: >=3.8
47
+ Description-Content-Type: text/markdown
48
+ License-File: LICENSE
49
+ Requires-Dist: lark>=1.1.0
50
+ Requires-Dist: jsonpickle>=3.0.0
51
+ Requires-Dist: msgpack>=1.0.0
52
+ Dynamic: license-file
53
+
54
+ # PyperCache
55
+
56
+ A Python library providing durable file-backed caching for JSON-like data with pluggable storage backends (pickle, JSON, chunked manifest, SQLite), optional TTL and staleness semantics, read-only query navigation, and append-only request logging.
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ pip install pypercache
62
+ ```
63
+
64
+ Or install from source:
65
+
66
+ ```bash
67
+ git clone https://github.com/BrandonBahret/PyperCache.git
68
+ cd PyperCache
69
+ pip install .
70
+ ```
71
+
72
+ ## Quick Start
73
+
74
+ See [docs/README.md](docs/README.md) for detailed documentation, examples, and API reference.
75
+
76
+ ## Features
77
+
78
+ - **Pluggable Backends**: Choose storage by file extension (.pkl, .json, .manifest, .db)
79
+ - **TTL & Staleness**: Optional expiry and acceptable staleness windows
80
+ - **Typed Objects**: Decorate classes for automatic serialization/deserialization
81
+ - **Query Navigation**: Safe, read-only JSON path queries with filters
82
+ - **Request Logging**: Thread-safe JSONL audit trails
83
+
84
+ ## Testing
85
+
86
+ ```bash
87
+ pytest
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT License (see LICENSE file)
@@ -0,0 +1,28 @@
1
+ PyperCache/__init__.py,sha256=U8Xw6bNZ-VeULGE-E-Z5OvnrZ0HPXEVAGRtIn4RCJiY,657
2
+ PyperCache/py.typed,sha256=8ZJUsxZiuOy1oJeVhsTWQhTG_6pTVHVXk5hJL79ebTk,25
3
+ PyperCache/core/__init__.py,sha256=0zQA6ycgsWsk4aYtQRaJdfzavkjWYmDeM1cwHgP02E8,298
4
+ PyperCache/core/cache.py,sha256=WkFqRHFLSRKJzTWo93eU8q3BFi-88jXMp7DT7gCAkB8,4513
5
+ PyperCache/core/cache_record.py,sha256=QBv75MZMZ0lbUFKLC6YFQFDYBEF1Z25vStl68NBfqZs,7859
6
+ PyperCache/core/request_logger.py,sha256=h1yIs05NGXftY5nvbS_71MhKeVcx6YAOAZ5X4BlYwC0,3815
7
+ PyperCache/models/apimodel.py,sha256=WGJaosVwpVNfveVayOPy00u4O69q-zgFV-k_C61ywY0,1586
8
+ PyperCache/query/__init__.py,sha256=oNrrJY8JjY9CykEAxSUvtXHClXXbNLDl645xc9WA8cI,157
9
+ PyperCache/query/json_injester.py,sha256=Gv7CH-hfia5SU7kvtf0Dw_9yRa6pfcYq5l-sq1dYwxI,16149
10
+ PyperCache/storage/__init__.py,sha256=93Bb-IS5RDA-A3ScwBEa7Ow4at26h0yEm8ryG4j3q1w,616
11
+ PyperCache/storage/backends.py,sha256=BNwpSFh-644zsSccwpAqPACo754qYabThdp8XryQbmA,3845
12
+ PyperCache/storage/base.py,sha256=gGupofw3HrnQyxiLpR0etP74_kyYHyYrLZaX30t8NkM,3961
13
+ PyperCache/storage/chunked_dictionary.py,sha256=oWWif-WSNNeMSBwNibWXeop8f9zQZu0ztuo9tv2lrDw,10934
14
+ PyperCache/storage/factory.py,sha256=HXPpX6qS4UrlD7RrGMolCgGnnaTvd9id568rayrJrAQ,1354
15
+ PyperCache/storage/sqlite_storage.py,sha256=K_PIZSEJyjaSEpHNzZr35DcaoKTiBM_28PRxXe6uRKk,18293
16
+ PyperCache/utils/__init__.py,sha256=_f4hwfeYB7j-lmLZ5CmWNq84z33rjzkFiMK9CIgFC5A,839
17
+ PyperCache/utils/collections.py,sha256=nAh4vPLPtgFvEX5BBuLIUseMjjB_F5lAhILXluQYUio,832
18
+ PyperCache/utils/fs.py,sha256=fYAVYwXOKKOHTkMVO7qA-1UnhlDNs8tNEnSJW5-famE,1354
19
+ PyperCache/utils/patterns.py,sha256=pJ9qoGdDGz53GPJALXXzkaPSeH1KrLeBWfkoMoaaePs,3104
20
+ PyperCache/utils/profiling.py,sha256=aSlkwlJzCqOAUnrNj9mV8IW_nUSrjwemzAPV_BDYFlE,1315
21
+ PyperCache/utils/sentinel.py,sha256=26B54gYedOe64NIDMv1-jeYE0BBZjCL9m_2qQkbADmA,728
22
+ PyperCache/utils/serialization.py,sha256=ZdjfekUlyCXXyPzSJEVZ36b5Y2RzWXGYw4btDgjBCJY,6233
23
+ PyperCache/utils/typing_cast.py,sha256=CmLy6OYDcpn_3AzBTeYt_dmvx4oHqjf5YaxEawY-1r0,2401
24
+ pypercache-0.1.0.dist-info/licenses/LICENSE,sha256=PxfCf-1Xl_lqG-cfZfSN9sc6OBOs2OqaZSyx04G40ZQ,1090
25
+ pypercache-0.1.0.dist-info/METADATA,sha256=d6Nb3EXTDmOKGyBPJOirpMq-DvqVeM5dHrITJ8Gtw_s,3838
26
+ pypercache-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
27
+ pypercache-0.1.0.dist-info/top_level.txt,sha256=HzL4WBDoVJe-T-2AuMgzNBlpYRENZ2MUirlkF1yxcjA,11
28
+ pypercache-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brandon Bahret
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 @@
1
+ PyperCache