predcache 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.
predcache/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """predcache: persistent prediction caching and batched inference for ML models."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .cache import PredictionCache, CacheEntry
6
+ from .batched import BatchedInferenceRunner, InferenceFn
7
+ from .windowing import FeatureWindowAssembler
8
+
9
+ __all__ = [
10
+ "PredictionCache",
11
+ "CacheEntry",
12
+ "BatchedInferenceRunner",
13
+ "FeatureWindowAssembler",
14
+ ]
predcache/batched.py ADDED
@@ -0,0 +1,114 @@
1
+ """Batched inference runner.
2
+
3
+ Wraps any model callable in batching + progress reporting, decoupling the
4
+ *scoring* of a feature matrix from the *loop* that feeds it. In the source
5
+ production system this exact pattern (batch_size=500 across three Keras
6
+ classifiers) turned hours-long full-range backtests into minutes by making
7
+ the cost of inference independent of how many bars were requested.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import time
13
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence
14
+
15
+ import numpy as np
16
+
17
+ __all__ = ["BatchedInferenceRunner", "InferenceFn"]
18
+
19
+ # A scorer takes (start, stop) and returns an array-like of predictions
20
+ # for that slice. Returning (n_samples, n_outputs) is typical.
21
+ InferenceFn = Callable[[int, int], Any]
22
+
23
+ DEFAULT_BATCH_SIZE = 500
24
+
25
+
26
+ class BatchedInferenceRunner:
27
+ """Run a scorer over *n_samples* rows in fixed batches.
28
+
29
+ Parameters
30
+ ----------
31
+ n_samples
32
+ Total number of feature rows to score.
33
+ inference_fn
34
+ Callable ``(start, stop) -> array-like`` that scores rows
35
+ ``[start, stop)`` of the pre-built feature matrix.
36
+ batch_size
37
+ Rows per scoring call. 500 was tuned on the source system
38
+ (Keras predict overhead vs memory footprint); any positive int works.
39
+ progress_every
40
+ Emit a progress dict every *progress_every* batches via the
41
+ ``progress`` callback (or collect them in ``self.progress_log``).
42
+
43
+ Examples
44
+ --------
45
+ >>> import numpy as np
46
+ >>> features = np.random.rand(1000, 35).astype(np.float32)
47
+ >>> runner = BatchedInferenceRunner(
48
+ ... n_samples=len(features),
49
+ ... inference_fn=lambda s, e: model.predict(features[s:e], verbose=0),
50
+ ... batch_size=500,
51
+ ... )
52
+ >>> preds = runner.run()
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ n_samples: int,
58
+ inference_fn: InferenceFn,
59
+ batch_size: int = DEFAULT_BATCH_SIZE,
60
+ progress_every: int = 20,
61
+ ):
62
+ if batch_size <= 0:
63
+ raise ValueError("batch_size must be positive")
64
+ self.n_samples = int(n_samples)
65
+ self.inference_fn = inference_fn
66
+ self.batch_size = int(batch_size)
67
+ self.progress_every = max(1, int(progress_every))
68
+ self.progress_log: List[Dict[str, Any]] = []
69
+
70
+ def batches(self) -> Iterator[tuple]:
71
+ start = 0
72
+ while start < self.n_samples:
73
+ stop = min(start + self.batch_size, self.n_samples)
74
+ yield start, stop
75
+ start = stop
76
+
77
+ def run(self, progress_cb: Optional[Callable[[Dict[str, Any]], None]] = None) -> List[Any]:
78
+ """Score all rows; returns a list of per-row predictions."""
79
+ out: List[Any] = []
80
+ t0 = time.perf_counter()
81
+ n_batches = 0
82
+ for start, stop in self.batches():
83
+ chunk = self.inference_fn(start, stop)
84
+ out.extend(_row_iter(chunk))
85
+ n_batches += 1
86
+ if progress_cb and n_batches % self.progress_every == 0:
87
+ cb = {
88
+ "rows_done": stop,
89
+ "rows_total": self.n_samples,
90
+ "batches": n_batches,
91
+ "elapsed_s": round(time.perf_counter() - t0, 3),
92
+ }
93
+ self.progress_log.append(cb)
94
+ progress_cb(cb)
95
+ return out
96
+
97
+
98
+ def _row_iter(chunk: Any) -> Iterator[Any]:
99
+ arr = np.asarray(chunk)
100
+ if arr.ndim == 1:
101
+ for v in arr:
102
+ yield v.item() if hasattr(v, "item") else v
103
+ else:
104
+ for row in arr:
105
+ yield row
106
+
107
+
108
+ def stack_features(matrices: Sequence[Any]) -> np.ndarray:
109
+ """Concatenate per-bar feature rows into one matrix (float32).
110
+
111
+ Utility used before calling a runner; keeps dtype consistent so
112
+ framework predict() calls do not re-copy the array.
113
+ """
114
+ return np.concatenate([np.asarray(m, dtype=np.float32) for m in matrices], axis=0)
predcache/cache.py ADDED
@@ -0,0 +1,220 @@
1
+ """Persistent, timestamp-keyed prediction cache.
2
+
3
+ Extracted and generalized from a production trading system (Neuromomentum /
4
+ ExtremePrediction) where full-range backtests over 100k+ bars were dominated
5
+ by redundant neural-network inference. Storing predictions keyed by bar
6
+ timestamp (not row index) lets any sub-range be replayed without offset
7
+ alignment and lets incremental backtests reuse everything previously computed.
8
+
9
+ Design goals
10
+ ------------
11
+ * **Correctness first** — atomic writes (temp file + os.replace), so a crash
12
+ mid-save never corrupts the cache.
13
+ * **Backend-agnostic** — pickle (default) and parquet backends ship in the
14
+ box; register your own with :func:`register_backend`.
15
+ * **Framework-agnostic** — works with any callable batch scorer (Keras,
16
+ PyTorch, ONNX, sklearn, plain numpy).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import pickle
23
+ import tempfile
24
+ from dataclasses import dataclass, field
25
+ from typing import Any, Callable, Dict, Hashable, Iterable, List, Optional, Protocol
26
+
27
+ __all__ = ["PredictionCache", "CacheEntry", "register_backend"]
28
+
29
+
30
+ class _Backend(Protocol):
31
+ """A cache backend persists a mapping of keys to payloads."""
32
+
33
+ def dump(self, entries: Dict[Hashable, Any], path: str) -> None: ...
34
+
35
+ def load(self, path: str) -> Dict[Hashable, Any]: ...
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Built-in backends
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def _pickle_dump(entries: Dict[Hashable, Any], path: str) -> None:
43
+ with open(path, "wb") as f:
44
+ pickle.dump(entries, f, protocol=pickle.HIGHEST_PROTOCOL)
45
+
46
+
47
+ def _pickle_load(path: str) -> Dict[Hashable, Any]:
48
+ with open(path, "rb") as f:
49
+ data = pickle.load(f)
50
+ return data if isinstance(data, dict) else {}
51
+
52
+
53
+ def _parquet_dump(entries: Dict[Hashable, Any], path: str) -> None:
54
+ import pandas as pd # optional dependency
55
+
56
+ keys = list(entries.keys())
57
+ df = pd.DataFrame({"key": keys, "value": [entries[k] for k in keys]})
58
+ df.to_parquet(path)
59
+
60
+
61
+ def _parquet_load(path: str) -> Dict[Hashable, Any]:
62
+ import pandas as pd # optional dependency
63
+
64
+ df = pd.read_parquet(path)
65
+ return dict(zip(df["key"], df["value"]))
66
+
67
+
68
+ _BACKENDS: Dict[str, _Backend] = {
69
+ "pickle": (_pickle_dump, _pickle_load),
70
+ "parquet": (_parquet_dump, _parquet_load),
71
+ }
72
+
73
+
74
+ def register_backend(name: str, dump: Callable, load: Callable) -> None:
75
+ """Register a custom cache backend (dump/load callables)."""
76
+ _BACKENDS[name] = (dump, load)
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class CacheEntry:
81
+ """One cached prediction, keyed by a timestamp or other hashable."""
82
+
83
+ key: Hashable
84
+ value: Any
85
+ meta: Dict[str, Any] = None # type: ignore[assignment]
86
+
87
+ def __post_init__(self):
88
+ if self.meta is None:
89
+ object.__setattr__(self, "meta", {})
90
+
91
+
92
+ class PredictionCache:
93
+ """Persistent cache for model predictions keyed by bar timestamp.
94
+
95
+ Parameters
96
+ ----------
97
+ path
98
+ File path of the cache file. Created on first save.
99
+ backend
100
+ ``"pickle"`` (default, zero-dependency) or ``"parquet"``
101
+ (requires ``pyarrow``; compact for numeric payloads).
102
+ safe_save
103
+ Write via a temp file + :func:`os.replace` so a crash never
104
+ corrupts an existing cache. Default ``True``.
105
+
106
+ Notes
107
+ -----
108
+ Keys should be timestamps or any other stable hashable identity —
109
+ *not* row indices. Index keys silently break when a frame is sliced
110
+ or resampled; timestamp keys survive any sub-range extraction.
111
+
112
+ Examples
113
+ --------
114
+ >>> from predcache import PredictionCache
115
+ >>> cache = PredictionCache("preds.pkl")
116
+ >>> cache.get_or_compute(
117
+ ... keys=["2026-01-02 10:00", "2026-01-02 10:01"],
118
+ ... compute_fn=lambda missing: {k: float(k[-4:]) for k in missing},
119
+ ... )
120
+ {'2026-01-02 10:01': 10.01, '2026-01-02 10:01': 10.01}
121
+ """
122
+
123
+ def __init__(self, path: str, backend: str = "pickle"):
124
+ self.path = path
125
+ if backend not in _BACKENDS:
126
+ raise ValueError(
127
+ f"Unknown backend {backend!r}. Registered: {sorted(_BACKENDS)}"
128
+ )
129
+ self._dump, self._load = _BACKENDS[backend]
130
+ self._entries: Dict[Hashable, Any] = {}
131
+ self._dirty = False
132
+
133
+ # -- I/O -----------------------------------------------------------------
134
+
135
+ def load(self) -> "PredictionCache":
136
+ if os.path.exists(self.path):
137
+ self._entries = self._load(self.path)
138
+ return self
139
+
140
+ def save(self) -> None:
141
+ if not self._dirty:
142
+ return
143
+ directory = os.path.dirname(self.path)
144
+ if directory:
145
+ os.makedirs(directory, exist_ok=True)
146
+ if self.path.endswith(".tmp"):
147
+ self._dump(self._entries, self.path)
148
+ else:
149
+ fd, tmp = tempfile.mkstemp(
150
+ dir=directory or ".", prefix=os.path.basename(self.path), suffix=".tmp"
151
+ )
152
+ os.close(fd)
153
+ try:
154
+ self._dump(self._entries, tmp)
155
+ os.replace(tmp, self.path)
156
+ finally:
157
+ if os.path.exists(tmp):
158
+ os.remove(tmp)
159
+ self._dirty = False
160
+
161
+ # -- lookup / mutation -----------------------------------------------------
162
+
163
+ def __contains__(self, key: Hashable) -> bool:
164
+ return key in self._entries
165
+
166
+ def __len__(self) -> int:
167
+ return len(self._entries)
168
+
169
+ def get(self, key: Hashable, default: Any = None) -> Any:
170
+ return self._entries.get(key, default)
171
+
172
+ def put(self, key: Hashable, value: Any) -> None:
173
+ self._entries[key] = value
174
+ self._dirty = True
175
+
176
+ def keys(self) -> Iterable[Hashable]:
177
+ return self._entries.keys()
178
+
179
+ def items(self) -> Iterable[tuple]:
180
+ return self._entries.items()
181
+
182
+ def info(self) -> Dict[str, Any]:
183
+ return {
184
+ "path": self.path,
185
+ "entries": len(self._entries),
186
+ "backend": "pickle" if self.path.endswith(".pkl") else "auto",
187
+ }
188
+
189
+ # -- the core convenience ---------------------------------------------------
190
+
191
+ def get_or_compute(
192
+ self,
193
+ keys: List[Hashable],
194
+ compute_fn: Callable[[List[Hashable]], Dict[Hashable, Any]],
195
+ save: bool = True,
196
+ ) -> Dict[Hashable, Any]:
197
+ """Return predictions for *keys*, computing only the missing ones.
198
+
199
+ ``compute_fn`` receives the list of missing keys and must return a
200
+ dict ``{key: value}``. Computed values are merged into the cache.
201
+ """
202
+ missing = [k for k in keys if k not in self._entries]
203
+ if missing:
204
+ computed = compute_fn(missing) or {}
205
+ for k, v in computed.items():
206
+ self._entries[k] = v
207
+ self._dirty = True
208
+ if save:
209
+ self.save()
210
+ return {k: self._entries[k] for k in keys if k in self._entries}
211
+
212
+ def drop(self, keys: List[Hashable]) -> int:
213
+ removed = 0
214
+ for k in keys:
215
+ if k in self._entries:
216
+ del self._entries[k]
217
+ removed += 1
218
+ if removed:
219
+ self._dirty = True
220
+ return removed
predcache/windowing.py ADDED
@@ -0,0 +1,90 @@
1
+ """Feature-window assembly helpers.
2
+
3
+ Generalizes the windowing pattern from the source trading system: for each
4
+ bar at index *i*, build a feature vector from a look-back window ending at
5
+ *i*, optionally reversed (most-recent-first, the convention used when the
6
+ models were trained on MT5 series ordering), then normalize.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Callable, List, Optional, Sequence
12
+
13
+ import numpy as np
14
+
15
+ __all__ = ["normalize_absolute", "FeatureWindowAssembler"]
16
+
17
+
18
+ def normalize_absolute(vec: np.ndarray) -> np.ndarray:
19
+ """Scale a vector so every element lies in [-1, 1] (L-inf normalization).
20
+
21
+ Matches the source system's ``normalize_vector_absolute``: divide by the
22
+ max absolute value, preserving sign and zeros. Returns the input
23
+ unchanged when its max abs value is 0.
24
+ """
25
+ vec = np.asarray(vec, dtype=np.float32)
26
+ scale = np.max(np.abs(vec))
27
+ if scale == 0:
28
+ return vec
29
+ return vec / scale
30
+
31
+
32
+ class FeatureWindowAssembler:
33
+ """Build look-back feature windows from an OHLC frame.
34
+
35
+ Parameters
36
+ ----------
37
+ window
38
+ Look-back length in bars (``bars_to_fetch`` in the source system).
39
+ warmup
40
+ Number of initial bars to skip before emitting windows.
41
+ row_fn
42
+ Callable ``(window_df) -> 1-D array`` producing the feature vector
43
+ for one window. Receives the reversed (most-recent-first) window
44
+ slice. This keeps domain feature logic (fractal detection, price
45
+ deltas, external regressors) in *your* code — the assembler only
46
+ handles looping, warmup and stacking.
47
+
48
+ Examples
49
+ --------
50
+ >>> assembler = FeatureWindowAssembler(window=50, warmup=10, row_fn=my_features)
51
+ >>> X = assembler.build(df) # (n_bars - warmup, n_features)
52
+ >>> keys = assembler.keys(df) # matching timestamp keys
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ window: int,
58
+ warmup: int = 0,
59
+ row_fn: Optional[Callable] = None,
60
+ ):
61
+ if window <= 0:
62
+ raise ValueError("window must be positive")
63
+ if warmup < 0:
64
+ raise ValueError("warmup must be >= 0")
65
+ self.window = int(window)
66
+ self.warmup = int(warmup)
67
+ self.row_fn = row_fn
68
+
69
+ def windows(self, df) -> List:
70
+ """Yield reversed (most-recent-first) window slices starting at warmup."""
71
+ n = len(df)
72
+ for i in range(self.warmup, n):
73
+ lo = max(0, i - (self.window - 1))
74
+ yield df.iloc[lo : i + 1].iloc[::-1].reset_index(drop=True)
75
+
76
+ def keys(self, df, key_col: str = "time") -> List:
77
+ """Timestamp keys for each window (stable identity for caching)."""
78
+ return [df.iloc[i][key_col] for i in range(self.warmup, len(df))]
79
+
80
+ def build(self, df, key_col: Optional[str] = None):
81
+ """Stack feature rows for every window; returns (X, keys)."""
82
+ if self.row_fn is None:
83
+ raise ValueError("row_fn is required to build features")
84
+ rows: List[np.ndarray] = []
85
+ for win in self.windows(df):
86
+ rows.append(np.asarray(self.row_fn(win), dtype=np.float32))
87
+ if not rows:
88
+ return np.zeros((0, 0), dtype=np.float32), []
89
+ keys = self.keys(df, key_col) if key_col else self.keys(df)
90
+ return np.stack(rows, axis=0), keys
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: predcache
3
+ Version: 0.1.0
4
+ Summary: Persistent timestamp-keyed prediction cache and batched inference runner for ML models
5
+ Author-email: Anton Uralskii <uralskyanton@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Geomanti/predcache
8
+ Project-URL: Repository, https://github.com/Geomanti/predcache
9
+ Project-URL: Issues, https://github.com/Geomanti/predcache/issues
10
+ Project-URL: Changelog, https://github.com/Geomanti/predcache/blob/main/CHANGELOG.md
11
+ Keywords: cache,inference,batching,machine-learning,backtesting,time-series,mlops
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy>=1.21
28
+ Provides-Extra: parquet
29
+ Requires-Dist: pandas>=1.5; extra == "parquet"
30
+ Requires-Dist: pyarrow>=10; extra == "parquet"
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest>=7; extra == "test"
33
+ Requires-Dist: pandas>=1.5; extra == "test"
34
+ Dynamic: license-file
35
+
36
+ # predcache
37
+
38
+ **Persistent timestamp-keyed prediction cache and batched inference runner for ML models.**
39
+
40
+ Extracted and generalized from a production neural-network trading system where
41
+ full-range backtests over 100,000+ bars were dominated by redundant model
42
+ inference. Storing predictions keyed by **bar timestamp** (not row index) makes
43
+ any sub-range replayable without offset alignment — and makes incremental
44
+ backtests reuse everything previously computed. The result: full-range runs
45
+ went from **hours to minutes**.
46
+
47
+ ## Why
48
+
49
+ Typical ML backtesting pipelines recompute model inference for the same bars on
50
+ every run — every parameter sweep, every range extension, every "what if".
51
+ `predcache` decouples the two expensive parts:
52
+
53
+ 1. **Feature-window assembly** — building look-back feature rows from an OHLC
54
+ frame (most-recent-first, warmup-aware), with your domain logic plugged in
55
+ via `row_fn`.
56
+ 2. **Batched inference** — scoring rows through *any* model callable
57
+ (Keras, PyTorch, ONNX, sklearn) in fixed batches with progress reporting.
58
+ 3. **Persistent caching** — predictions keyed by timestamp, saved atomically,
59
+ so the next run computes only what's missing.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install predcache # core (numpy only)
65
+ pip install predcache[parquet] # + parquet backend
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ import pandas as pd
72
+ from predcache import PredictionCache, BatchedInferenceRunner, FeatureWindowAssembler
73
+
74
+ # 1. Assemble feature windows (your domain logic stays in row_fn)
75
+ assembler = FeatureWindowAssembler(
76
+ window=50, warmup=500,
77
+ row_fn=lambda win: my_feature_logic(win), # 1-D feature vector per window
78
+ )
79
+ X, keys = assembler.build(df, key_col="time") # X: (n_bars - warmup, n_features)
80
+
81
+ # 2. Batched inference through any model
82
+ runner = BatchedInferenceRunner(
83
+ n_samples=len(X),
84
+ inference_fn=lambda s, e: model.predict(X[s:e], verbose=0),
85
+ batch_size=500,
86
+ )
87
+ preds = runner.run()
88
+
89
+ # 3. Cache by timestamp — next run computes only missing bars
90
+ cache = PredictionCache("predictions.pkl")
91
+ out = cache.get_or_compute(
92
+ keys=keys,
93
+ compute_fn=lambda missing: predict_bars(X, keys, missing),
94
+ )
95
+ ```
96
+
97
+ ## Features
98
+
99
+ - **Timestamp-keyed cache** — sub-range extraction without offset alignment;
100
+ row-index caches silently break on slicing/resampling, timestamp keys don't.
101
+ - **Atomic saves** — temp file + `os.replace`; a crash mid-save never corrupts
102
+ an existing cache.
103
+ - **Pluggable backends** — pickle (zero-dependency) and parquet ship in the
104
+ box; register custom backends with `register_backend(name, dump, load)`.
105
+ - **Batched inference runner** — framework-agnostic `(start, stop) -> scores`
106
+ contract, progress callbacks, tuned default batch size (500).
107
+ - **L-inf normalization** helper matching the source system's
108
+ `normalize_vector_absolute`.
109
+
110
+ ## Origin
111
+
112
+ This package is the generalized core of the prediction-serving layer of
113
+ [Neuromomentum](https://github.com/Geomanti)'s production trading system
114
+ (three Keras classifiers, MT5 live execution, 2012–2026 backtest range). The
115
+ original cache manager was 542 lines of domain-specific code; what ships here
116
+ is the reusable pattern: timestamp-keyed persistence, compute-only-missing,
117
+ batched scoring, windowed feature assembly.
118
+
119
+ ## License
120
+
121
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,9 @@
1
+ predcache/__init__.py,sha256=6ygXlSDqtYnwfE9FjwR2RHJ1FtDW1mG_l7LPxJT6MCI,373
2
+ predcache/batched.py,sha256=9KX2oMEzjsN-WPPLydEGiV2IzR7SKKPkGn1iX4IFwww,3907
3
+ predcache/cache.py,sha256=RHsHZM8h7fpbxsFPXrzxKNgXjv61MlfMpmuW1und224,7223
4
+ predcache/windowing.py,sha256=f6VNt8EIOeXY4teukUQlXyZUwzBXZtCtirs8H4UUFfA,3314
5
+ predcache-0.1.0.dist-info/licenses/LICENSE,sha256=IImUGSfeYtg7nXjxl9JJUnofX8XsYfeOWThilV4cHn8,1070
6
+ predcache-0.1.0.dist-info/METADATA,sha256=omrs49Tw7afub0EdOcvLDe9mBKRwyLDW01vuntPDcls,4883
7
+ predcache-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ predcache-0.1.0.dist-info/top_level.txt,sha256=zu1kFhCpeR0jg0dWN4SsytadJ0ZCkiPsB8bJiQOBe8Q,10
9
+ predcache-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anton Uralskii
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
+ predcache