ragtools 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.
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.3
2
+ Name: ragtools
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Wannes Vantorre
6
+ Author-email: Wannes Vantorre <vantorrewannes@gmail.com>
7
+ Requires-Dist: blake3>=1.0.9
8
+ Requires-Dist: dill>=0.4.1
9
+ Requires-Dist: faiss-cpu>=1.15.1
10
+ Requires-Dist: scipy>=1.18.1
11
+ Requires-Dist: semantic-chunker>=0.2.0
12
+ Requires-Dist: sentence-transformers>=6.0.1
13
+ Requires-Dist: tokenizers>=0.23.2
14
+ Requires-Dist: transformers>=5.17.0
15
+ Requires-Dist: unstructured-inference>=1.6.13
16
+ Requires-Dist: unstructured[csv,md,pdf]>=0.27.5
17
+ Requires-Dist: torch>=2.13.0 ; extra == 'cpu'
18
+ Requires-Dist: torchvision>=0.28.0 ; extra == 'cpu'
19
+ Requires-Python: >=3.14
20
+ Project-URL: Homepage, https://github.com/VantorreWannes/ragtools
21
+ Project-URL: Repository, https://github.com/VantorreWannes/ragtools
22
+ Project-URL: Issues, https://github.com/VantorreWannes/ragtools/issues
23
+ Project-URL: Changelog, https://github.com/VantorreWannes/ragtools/releases
24
+ Provides-Extra: cpu
25
+ Description-Content-Type: text/markdown
26
+
27
+ # ragtools
28
+
29
+ Simple cache based RAG primitives
@@ -0,0 +1,3 @@
1
+ # ragtools
2
+
3
+ Simple cache based RAG primitives
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "ragtools"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "blake3>=1.0.9",
9
+ "dill>=0.4.1",
10
+ "faiss-cpu>=1.15.1",
11
+ "scipy>=1.18.1",
12
+ "semantic-chunker>=0.2.0",
13
+ "sentence-transformers>=6.0.1",
14
+ "tokenizers>=0.23.2",
15
+ "transformers>=5.17.0",
16
+ "unstructured-inference>=1.6.13",
17
+ "unstructured[csv,md,pdf]>=0.27.5",
18
+ ]
19
+
20
+ [[project.authors]]
21
+ name = "Wannes Vantorre"
22
+ email = "vantorrewannes@gmail.com"
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/VantorreWannes/ragtools"
26
+ Repository = "https://github.com/VantorreWannes/ragtools"
27
+ Issues = "https://github.com/VantorreWannes/ragtools/issues"
28
+ Changelog = "https://github.com/VantorreWannes/ragtools/releases"
29
+
30
+ [project.optional-dependencies]
31
+ cpu = [
32
+ "torch>=2.13.0",
33
+ "torchvision>=0.28.0",
34
+ ]
35
+
36
+ [build-system]
37
+ requires = ["uv_build>=0.12.13,<0.13.0"]
38
+ build-backend = "uv_build"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest-asyncio>=1.4.0",
43
+ "pytest-cov>=7.1.0",
44
+ "ruff>=0.16.7",
45
+ "ty>=0.0.80",
46
+ ]
47
+
48
+ [tool.pytest.ini_options]
49
+ markers = [
50
+ "smoke",
51
+ "unit",
52
+ "integration",
53
+ "application",
54
+ ]
55
+
56
+ [[tool.uv.index]]
57
+ name = "pytorch-cpu"
58
+ url = "https://download.pytorch.org/whl/cpu"
59
+ explicit = true
60
+
61
+ [[tool.uv.sources.torch]]
62
+ index = "pytorch-cpu"
63
+ extra = "cpu"
64
+
65
+ [[tool.uv.sources.torchvision]]
66
+ index = "pytorch-cpu"
67
+ extra = "cpu"
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "ragtools"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Wannes Vantorre", email = "vantorrewannes@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "blake3>=1.0.9",
12
+ "dill>=0.4.1",
13
+ "faiss-cpu>=1.15.1",
14
+ "scipy>=1.18.1",
15
+ "semantic-chunker>=0.2.0",
16
+ "sentence-transformers>=6.0.1",
17
+ "tokenizers>=0.23.2",
18
+ "transformers>=5.17.0",
19
+ "unstructured-inference>=1.6.13",
20
+ "unstructured[csv,md,pdf]>=0.27.5",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/VantorreWannes/ragtools"
25
+ Repository = "https://github.com/VantorreWannes/ragtools"
26
+ Issues = "https://github.com/VantorreWannes/ragtools/issues"
27
+ Changelog = "https://github.com/VantorreWannes/ragtools/releases"
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.12.13,<0.13.0"]
31
+ build-backend = "uv_build"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "pytest-asyncio>=1.4.0",
36
+ "pytest-cov>=7.1.0",
37
+ "ruff>=0.16.7",
38
+ "ty>=0.0.80",
39
+ ]
40
+
41
+ [tool.pytest.ini_options]
42
+ markers = [
43
+ "smoke",
44
+ "unit",
45
+ "integration",
46
+ "application",
47
+ ]
48
+
49
+
50
+ [[tool.uv.index]]
51
+ name = "pytorch-cpu"
52
+ url = "https://download.pytorch.org/whl/cpu"
53
+ explicit = true
54
+
55
+ [project.optional-dependencies]
56
+ cpu = [
57
+ "torch>=2.13.0",
58
+ "torchvision>=0.28.0",
59
+ ]
60
+
61
+ [tool.uv.sources]
62
+ torch = [
63
+ { index = "pytorch-cpu", extra = "cpu" },
64
+ ]
65
+ torchvision = [
66
+ { index = "pytorch-cpu", extra = "cpu" },
67
+ ]
@@ -0,0 +1,44 @@
1
+ from ragtools.embedders import Embedder, SentenceTransformerEmbedder, SpladeEmbedder
2
+ from ragtools.fusers import BordaCountFuser, Fuser, ReciprocalRankFuser
3
+ from ragtools.generators import Generator, TransformersGenerator
4
+ from ragtools.indexes import FaissEmbeddingIndex, Index, SparseEmbeddingIndex
5
+ from ragtools.parsers import (
6
+ ChunkParser,
7
+ CsvFileElementParser,
8
+ ElementPageIndexParser,
9
+ ElementTextParser,
10
+ MdFileElementParser,
11
+ Parser,
12
+ PdfFileElementParser,
13
+ TextFileElementParser,
14
+ )
15
+ from ragtools.scorers import CrossEncoderScorer, Scorer
16
+ from ragtools.stores import DirectoryStore, FileStore, MemoryStore, Store
17
+
18
+ __all__ = [
19
+ "BordaCountFuser",
20
+ "ChunkParser",
21
+ "CrossEncoderScorer",
22
+ "CsvFileElementParser",
23
+ "DirectoryStore",
24
+ "ElementPageIndexParser",
25
+ "ElementTextParser",
26
+ "Embedder",
27
+ "FaissEmbeddingIndex",
28
+ "FileStore",
29
+ "Fuser",
30
+ "Generator",
31
+ "Index",
32
+ "MdFileElementParser",
33
+ "MemoryStore",
34
+ "Parser",
35
+ "PdfFileElementParser",
36
+ "ReciprocalRankFuser",
37
+ "Scorer",
38
+ "SentenceTransformerEmbedder",
39
+ "SparseEmbeddingIndex",
40
+ "SpladeEmbedder",
41
+ "Store",
42
+ "TextFileElementParser",
43
+ "TransformersGenerator",
44
+ ]
@@ -0,0 +1,34 @@
1
+ from dataclasses import dataclass
2
+ from functools import cached_property
3
+ from typing import Protocol, cast
4
+
5
+ from sentence_transformers import SentenceTransformer, SparseEncoder
6
+
7
+
8
+ class Embedder[E](Protocol):
9
+ def embed(self, chunk: str) -> E: ...
10
+
11
+
12
+ @dataclass
13
+ class SentenceTransformerEmbedder:
14
+ model_name: str
15
+
16
+ @cached_property
17
+ def model(self) -> SentenceTransformer:
18
+ return SentenceTransformer(self.model_name)
19
+
20
+ def embed(self, chunk: str) -> list[float]:
21
+ return self.model.encode(chunk, normalize_embeddings=True).tolist()
22
+
23
+
24
+ @dataclass
25
+ class SpladeEmbedder:
26
+ model_name: str
27
+
28
+ @cached_property
29
+ def model(self) -> SparseEncoder:
30
+ return SparseEncoder(self.model_name)
31
+
32
+ def embed(self, chunk: str) -> dict[str, float]:
33
+ embeddings = self.model.encode(chunk)
34
+ return dict(cast(list[tuple[str, float]], [*self.model.decode(embeddings)]))
@@ -0,0 +1,31 @@
1
+ from collections.abc import Hashable, Sequence
2
+ from dataclasses import dataclass
3
+ from typing import Protocol
4
+
5
+
6
+ class Fuser[V](Protocol):
7
+ def fuse(self, *values: Sequence[V]) -> list[V]: ...
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class ReciprocalRankFuser[V: Hashable]:
12
+ k: int = 60
13
+
14
+ def fuse(self, *values: Sequence[V]) -> list[V]:
15
+ scores: dict[V, float] = {}
16
+ for ranking in values:
17
+ for rank, value in enumerate(ranking, start=1):
18
+ weight = 1 / (self.k + rank)
19
+ scores[value] = scores.get(value, 0.0) + weight
20
+ return sorted(scores, key=lambda value: scores[value], reverse=True)
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class BordaCountFuser[V: Hashable]:
25
+ def fuse(self, *values: Sequence[V]) -> list[V]:
26
+ scores: dict[V, float] = {}
27
+ for ranking in values:
28
+ for rank, value in enumerate(ranking, start=1):
29
+ weight = len(ranking) - rank + 1
30
+ scores[value] = scores.get(value, 0.0) + weight
31
+ return sorted(scores, key=lambda value: scores[value], reverse=True)
@@ -0,0 +1,28 @@
1
+ from dataclasses import dataclass
2
+ from functools import cached_property
3
+ from typing import Protocol
4
+
5
+ from transformers import TextGenerationPipeline, pipeline
6
+
7
+
8
+ class Generator(Protocol):
9
+ def prompt(self, query: str) -> str: ...
10
+
11
+
12
+ @dataclass
13
+ class TransformersGenerator:
14
+ model_name: str
15
+ max_new_tokens: int
16
+
17
+ @cached_property
18
+ def model(self) -> TextGenerationPipeline:
19
+ return pipeline("text-generation", model=self.model_name)
20
+
21
+ def prompt(self, query: str) -> str:
22
+ chat = [{"role": "user", "content": query}]
23
+ outputs = self.model(
24
+ chat,
25
+ max_new_tokens=self.max_new_tokens,
26
+ do_sample=True,
27
+ )
28
+ return outputs[0]["generated_text"][-1]["content"]
@@ -0,0 +1,153 @@
1
+ import builtins
2
+ from collections.abc import Hashable
3
+ from typing import Protocol
4
+
5
+ import faiss
6
+ import numpy as np
7
+ from scipy.sparse import csr_matrix
8
+
9
+ from ragtools.stores import Store
10
+
11
+
12
+ class Index[K, V](Store[K, V], Protocol):
13
+ def closest(self, key: K, k: int) -> tuple[K, ...]: ...
14
+
15
+
16
+ class FaissEmbeddingIndex:
17
+ def __init__(self, dimensions: int) -> None:
18
+ self._index = faiss.IndexIDMap2(faiss.IndexFlatL2(dimensions))
19
+ self._ids: builtins.set[int] = builtins.set()
20
+
21
+ def set(self, key: int, value: list[float]) -> None:
22
+ self.delete(key)
23
+ self._index.add_with_ids(
24
+ np.ascontiguousarray([value], dtype=np.float32),
25
+ np.array([key], dtype=np.int64),
26
+ )
27
+ self._ids.add(key)
28
+
29
+ def get(self, key: int) -> list[float]:
30
+ if key not in self._ids:
31
+ raise KeyError(key)
32
+ return self._index.reconstruct(key).tolist()
33
+
34
+ def delete(self, key: int) -> None:
35
+ if key in self._ids:
36
+ ids = np.array([key], dtype=np.int64)
37
+ self._index.remove_ids(faiss.IDSelectorBatch(len(ids), faiss.swig_ptr(ids)))
38
+ self._ids.discard(key)
39
+
40
+ def contains(self, key: int) -> bool:
41
+ return key in self._ids
42
+
43
+ def keys(self) -> builtins.set[int]:
44
+ return builtins.set(self._ids)
45
+
46
+ def closest(self, key: int, k: int) -> tuple[int, ...]:
47
+ if k <= 0 or self._index.ntotal == 0:
48
+ return ()
49
+ if key not in self._ids:
50
+ raise KeyError(key)
51
+ vec = self._index.reconstruct(key)[None]
52
+ _, ids = self._index.search(vec, min(k + 1, self._index.ntotal))
53
+ return tuple(int(fid) for fid in ids[0] if fid != -1 and int(fid) != key)[:k]
54
+
55
+ def __len__(self) -> int:
56
+ return self._index.ntotal
57
+
58
+ def __contains__(self, key: int) -> bool:
59
+ return self.contains(key)
60
+
61
+
62
+ class SparseEmbeddingIndex[K: Hashable]:
63
+ def __init__(self) -> None:
64
+ self._data = np.empty(0, dtype=np.float32)
65
+ self._indices = np.empty(0, dtype=np.int32)
66
+ self._indptr = np.zeros(1, dtype=np.int64)
67
+ self._rows: list[K] = []
68
+ self._row_of: dict[K, int] = {}
69
+ self._vocab: dict[str, int] = {}
70
+ self._tokens: list[str] = []
71
+
72
+ def set(self, key: K, value: dict[str, float]) -> None:
73
+ self.delete(key)
74
+ clean = {t: w for t, w in value.items() if w}
75
+ cols = []
76
+ for t in clean:
77
+ if t not in self._vocab:
78
+ self._vocab[t] = len(self._tokens)
79
+ self._tokens.append(t)
80
+ cols.append(self._vocab[t])
81
+
82
+ self._data = np.concatenate(
83
+ [self._data, np.fromiter(clean.values(), np.float32, len(clean))]
84
+ )
85
+ self._indices = np.concatenate(
86
+ [self._indices, np.fromiter(cols, np.int32, len(clean))]
87
+ )
88
+ self._indptr = np.append(self._indptr, len(self._data))
89
+ self._row_of[key] = len(self._rows)
90
+ self._rows.append(key)
91
+
92
+ def get(self, key: K) -> dict[str, float]:
93
+ if key not in self._row_of:
94
+ raise KeyError(key)
95
+ r = self._row_of[key]
96
+ start, end = int(self._indptr[r]), int(self._indptr[r + 1])
97
+ return {
98
+ self._tokens[c]: float(w)
99
+ for c, w in zip(
100
+ self._indices[start:end], self._data[start:end], strict=True
101
+ )
102
+ }
103
+
104
+ def delete(self, key: K) -> None:
105
+ if (r := self._row_of.pop(key, None)) is None:
106
+ return
107
+ start, end = int(self._indptr[r]), int(self._indptr[r + 1])
108
+ self._data = np.delete(self._data, slice(start, end))
109
+ self._indices = np.delete(self._indices, slice(start, end))
110
+ counts = np.diff(self._indptr)
111
+ keep = np.ones(counts.size, dtype=bool)
112
+ keep[r] = False
113
+ self._indptr = np.concatenate(
114
+ [np.zeros(1, dtype=np.int64), np.cumsum(counts[keep], dtype=np.int64)]
115
+ )
116
+ del self._rows[r]
117
+ self._row_of = {k: i for i, k in enumerate(self._rows)}
118
+
119
+ def contains(self, key: K) -> bool:
120
+ return key in self._row_of
121
+
122
+ def keys(self) -> builtins.set[K]:
123
+ return builtins.set(self._row_of)
124
+
125
+ def closest(self, key: K, k: int) -> tuple[K, ...]:
126
+ if key not in self._row_of:
127
+ raise KeyError(key)
128
+ if k <= 0 or len(self._rows) <= 1 or len(self._vocab) == 0:
129
+ return ()
130
+ r = self._row_of[key]
131
+ start, end = int(self._indptr[r]), int(self._indptr[r + 1])
132
+ q = np.zeros(len(self._vocab), dtype=np.float32)
133
+ q[self._indices[start:end]] = self._data[start:end]
134
+ scores = self._matrix @ q
135
+ scores[r] = -np.inf
136
+ scores[scores == 0.0] = -np.inf
137
+ k_eff = min(k, len(self._rows) - 1)
138
+ top = np.argpartition(-scores, k_eff - 1)[:k_eff]
139
+ top = top[np.argsort(-scores[top], kind="stable")]
140
+ return tuple(self._rows[i] for i in top if scores[i] != -np.inf)
141
+
142
+ @property
143
+ def _matrix(self) -> csr_matrix:
144
+ return csr_matrix(
145
+ (self._data, self._indices, self._indptr),
146
+ shape=(len(self._rows), max(len(self._vocab), 1)),
147
+ )
148
+
149
+ def __len__(self) -> int:
150
+ return len(self._rows)
151
+
152
+ def __contains__(self, key: K) -> bool:
153
+ return self.contains(key)
@@ -0,0 +1,75 @@
1
+ from dataclasses import dataclass
2
+ from functools import cached_property
3
+ from pathlib import Path
4
+ from typing import Protocol, cast
5
+
6
+ from semantic_chunker import TextSplitter, get_chunker
7
+ from unstructured.documents.elements import Element
8
+ from unstructured.partition.csv import partition_csv
9
+ from unstructured.partition.md import partition_md
10
+ from unstructured.partition.pdf import partition_pdf
11
+ from unstructured.partition.text import partition_text
12
+
13
+
14
+ class Parser[D, V](Protocol):
15
+ def units(self, data: D) -> V: ...
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class CsvFileElementParser:
20
+ def units(self, data: Path) -> tuple[Element, ...]:
21
+ return tuple(partition_csv(str(data)))
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class MdFileElementParser:
26
+ def units(self, data: Path) -> tuple[Element, ...]:
27
+ return tuple(partition_md(str(data)))
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class PdfFileElementParser:
32
+ def units(self, data: Path) -> tuple[Element, ...]:
33
+ return tuple(partition_pdf(str(data)))
34
+
35
+
36
+ @dataclass(slots=True)
37
+ class TextFileElementParser:
38
+ def units(self, data: Path) -> tuple[Element, ...]:
39
+ return tuple(partition_text(str(data)))
40
+
41
+
42
+ @dataclass(slots=True)
43
+ class ElementTextParser:
44
+ def units(self, data: Element) -> str:
45
+ return data.text
46
+
47
+
48
+ @dataclass(slots=True)
49
+ class ElementPageIndexParser:
50
+ def units(self, data: Element) -> int:
51
+ page_index = data.metadata.page_number
52
+ return page_index if page_index is not None else -1
53
+
54
+ @dataclass
55
+ class ChunkParser:
56
+ model_name: str
57
+ chunk_size: int
58
+ overlap: int
59
+
60
+ @cached_property
61
+ def model(self) -> TextSplitter:
62
+ return cast(
63
+ "TextSplitter",
64
+ get_chunker(
65
+ self.model_name,
66
+ chunking_type="text",
67
+ tree_sitter_language=None,
68
+ max_tokens=self.chunk_size,
69
+ overlap=self.overlap,
70
+ trim=True,
71
+ ),
72
+ )
73
+
74
+ def units(self, data: str) -> tuple[str, ...]:
75
+ return tuple(self.model.chunks(data))
File without changes
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass
2
+ from functools import cached_property
3
+ from typing import Protocol
4
+
5
+ from sentence_transformers import CrossEncoder
6
+
7
+
8
+ class Scorer(Protocol):
9
+ def score(self, query: str, chunk: str) -> float: ...
10
+
11
+
12
+ @dataclass
13
+ class CrossEncoderScorer:
14
+ model_name: str
15
+
16
+ @cached_property
17
+ def model(self) -> CrossEncoder:
18
+ return CrossEncoder(self.model_name)
19
+
20
+ def score(self, query: str, chunk: str) -> float:
21
+ return float(self.model.predict((query, chunk)).tolist())
@@ -0,0 +1,118 @@
1
+ import builtins
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Protocol
5
+
6
+ import blake3
7
+ import dill
8
+
9
+
10
+ class Store[K, V](Protocol):
11
+ def set(self, key: K, value: V) -> None: ...
12
+ def get(self, key: K) -> V: ...
13
+ def delete(self, key: K) -> None: ...
14
+ def contains(self, key: K) -> bool: ...
15
+ def keys(self) -> builtins.set[K]: ...
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class MemoryStore[K, V]:
20
+ items: dict[K, V]
21
+
22
+ def __init__(self) -> None:
23
+ self.items = {}
24
+
25
+ def set(self, key: K, value: V) -> None:
26
+ self.items[key] = value
27
+
28
+ def get(self, key: K) -> V:
29
+ return self.items[key]
30
+
31
+ def delete(self, key: K) -> None:
32
+ del self.items[key]
33
+
34
+ def contains(self, key: K) -> bool:
35
+ return key in self.items
36
+
37
+ def keys(self) -> builtins.set[K]:
38
+ return set(self.items.keys())
39
+
40
+
41
+ @dataclass(slots=True)
42
+ class FileStore[K, V]:
43
+ file: Path
44
+
45
+ def _load_items(self) -> dict[K, V]:
46
+ if not self.file.is_file():
47
+ return {}
48
+ with self.file.open("rb") as f:
49
+ return dill.load(f)
50
+
51
+ def _save_items(self, items: dict[K, V]) -> None:
52
+ self.file.parent.mkdir(parents=True, exist_ok=True)
53
+ with self.file.open("wb") as f:
54
+ dill.dump(items, f)
55
+
56
+ def set(self, key: K, value: V) -> None:
57
+ items = self._load_items()
58
+ items[key] = value
59
+ self._save_items(items)
60
+
61
+ def get(self, key: K) -> V:
62
+ items = self._load_items()
63
+ return items[key]
64
+
65
+ def delete(self, key: K) -> None:
66
+ items = self._load_items()
67
+ del items[key]
68
+ self._save_items(items)
69
+
70
+ def contains(self, key: K) -> bool:
71
+ items = self._load_items()
72
+ return key in items
73
+
74
+ def keys(self) -> builtins.set[K]:
75
+ items = self._load_items()
76
+ return set(items.keys())
77
+
78
+
79
+ @dataclass(slots=True)
80
+ class DirectoryStore[K, V]:
81
+ directory: Path
82
+
83
+ def _file_path(self, key: K) -> Path:
84
+ return self.directory / blake3.blake3(dill.dumps(key)).hexdigest()
85
+
86
+ def set(self, key: K, value: V) -> None:
87
+ path = self._file_path(key)
88
+ self.directory.mkdir(parents=True, exist_ok=True)
89
+ with path.open("wb") as f:
90
+ dill.dump((key, value), f)
91
+
92
+ def get(self, key: K) -> V:
93
+ path = self._file_path(key)
94
+ if not path.is_file():
95
+ raise KeyError(key)
96
+ with path.open("rb") as f:
97
+ _, value = dill.load(f)
98
+ return value
99
+
100
+ def delete(self, key: K) -> None:
101
+ path = self._file_path(key)
102
+ if not path.is_file():
103
+ raise KeyError(key)
104
+ path.unlink()
105
+
106
+ def contains(self, key: K) -> bool:
107
+ return self._file_path(key).is_file()
108
+
109
+ def keys(self) -> builtins.set[K]:
110
+ if not self.directory.is_dir():
111
+ return set()
112
+ keys_set: set[K] = set()
113
+ for file in self.directory.iterdir():
114
+ if file.is_file():
115
+ with file.open("rb") as f:
116
+ stored_key, _ = dill.load(f)
117
+ keys_set.add(stored_key)
118
+ return keys_set