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.
- PyperCache/__init__.py +25 -0
- PyperCache/core/__init__.py +7 -0
- PyperCache/core/cache.py +126 -0
- PyperCache/core/cache_record.py +217 -0
- PyperCache/core/request_logger.py +107 -0
- PyperCache/models/apimodel.py +49 -0
- PyperCache/py.typed +1 -0
- PyperCache/query/__init__.py +10 -0
- PyperCache/query/json_injester.py +436 -0
- PyperCache/storage/__init__.py +28 -0
- PyperCache/storage/backends.py +106 -0
- PyperCache/storage/base.py +103 -0
- PyperCache/storage/chunked_dictionary.py +297 -0
- PyperCache/storage/factory.py +40 -0
- PyperCache/storage/sqlite_storage.py +485 -0
- PyperCache/utils/__init__.py +25 -0
- PyperCache/utils/collections.py +28 -0
- PyperCache/utils/fs.py +46 -0
- PyperCache/utils/patterns.py +97 -0
- PyperCache/utils/profiling.py +44 -0
- PyperCache/utils/sentinel.py +26 -0
- PyperCache/utils/serialization.py +175 -0
- PyperCache/utils/typing_cast.py +72 -0
- pypercache-0.1.0.dist-info/METADATA +92 -0
- pypercache-0.1.0.dist-info/RECORD +28 -0
- pypercache-0.1.0.dist-info/WHEEL +5 -0
- pypercache-0.1.0.dist-info/licenses/LICENSE +21 -0
- pypercache-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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
|