diskstore 0.2.0__tar.gz → 0.4.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
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025-2026 Wolfgang Langner
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -1,16 +1,18 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: diskstore
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Simple MutableMapping compatible SQLite based disk storage.
5
5
  Author: tds333
6
6
  Author-email: tds333 <tds333@mailbox.org>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE.txt
7
9
  Requires-Dist: apsw>=3.51.3
8
10
  Requires-Python: >=3.10
9
11
  Description-Content-Type: text/markdown
10
12
 
11
13
  # DiskStore Sqlite based disk store
12
14
 
13
- `DiskStore` is an Apache2/MIT licensed disk storage library, written
15
+ `DiskStore` is a BSD-3-Clause licensed disk storage library, written
14
16
  in pure Python.
15
17
 
16
18
  Inspired by DiskCache library it implements a MutableMapping compatible sqlite
@@ -85,16 +87,7 @@ faster sqlite library apsw with up to date sqlite library version.
85
87
  License
86
88
  -------
87
89
 
88
- Copyright 2025-2026 Wolfgang Langner
89
-
90
- Licensed under the Apache License, Version 2.0 (the "License") or MIT; you may not use
91
- this file except in compliance with the License. You may obtain a copy of the
92
- License at
90
+ `DiskStore` is distributed under the terms of the
91
+ [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html) license.
93
92
 
94
- http://www.apache.org/licenses/LICENSE-2.0
95
- https://mit-license.org/
96
-
97
- Unless required by applicable law or agreed to in writing, software distributed
98
- under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
99
- CONDITIONS OF ANY KIND, either express or implied. See the License for the
100
- specific language governing permissions and limitations under the License.
93
+ Copyright 2025-2026 Wolfgang Langner
@@ -1,6 +1,6 @@
1
1
  # DiskStore Sqlite based disk store
2
2
 
3
- `DiskStore` is an Apache2/MIT licensed disk storage library, written
3
+ `DiskStore` is a BSD-3-Clause licensed disk storage library, written
4
4
  in pure Python.
5
5
 
6
6
  Inspired by DiskCache library it implements a MutableMapping compatible sqlite
@@ -75,16 +75,7 @@ faster sqlite library apsw with up to date sqlite library version.
75
75
  License
76
76
  -------
77
77
 
78
- Copyright 2025-2026 Wolfgang Langner
79
-
80
- Licensed under the Apache License, Version 2.0 (the "License") or MIT; you may not use
81
- this file except in compliance with the License. You may obtain a copy of the
82
- License at
78
+ `DiskStore` is distributed under the terms of the
79
+ [BSD-3-Clause](https://spdx.org/licenses/BSD-3-Clause.html) license.
83
80
 
84
- http://www.apache.org/licenses/LICENSE-2.0
85
- https://mit-license.org/
86
-
87
- Unless required by applicable law or agreed to in writing, software distributed
88
- under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
89
- CONDITIONS OF ANY KIND, either express or implied. See the License for the
90
- specific language governing permissions and limitations under the License.
81
+ Copyright 2025-2026 Wolfgang Langner
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "diskstore"
3
+ version = "0.4.0"
4
+ description = "Simple MutableMapping compatible SQLite based disk storage."
5
+ readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+ license-files = ["LICENSE.txt"]
8
+ requires-python = ">=3.10"
9
+ dependencies = ["apsw>=3.51.3"]
10
+
11
+ [[project.authors]]
12
+ name = "tds333"
13
+ email = "tds333@mailbox.org"
14
+
15
+ [dependency-groups]
16
+ dev = [
17
+ "msgspec>=0.20.0",
18
+ "pydantic>=2.12.5,<=2.99.99b0",
19
+ "pytest",
20
+ "pytest-asyncio>=1.3.0",
21
+ "pytest-cov",
22
+ "pytest-examples>=0.0.18",
23
+ "pytest-xdist",
24
+ "structtype>=0.8.1",
25
+ ]
26
+ docs = [
27
+ "mkdocstrings[python]>=1.0.3",
28
+ "zensical>=0.0.23",
29
+ ]
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.9.2,<1"]
33
+ build-backend = "uv_build"
34
+
35
+ [tool.coverage.run]
36
+ parallel = true
37
+ branch = true
38
+ source = ["diskstore"]
39
+
40
+ [tool.coverage.report]
41
+ show_missing = true
42
+ skip_covered = true
43
+ exclude_lines = [
44
+ "no cov",
45
+ "if __name__ == .__main__.:",
46
+ "if TYPE_CHECKING:",
47
+ ]
48
+
49
+ [tool.pytest]
50
+ testpaths = ["tests"]
51
+
52
+ [tool.ruff]
53
+ exclude = [
54
+ ".env",
55
+ ".venv",
56
+ "venv",
57
+ ]
58
+ line-length = 88
59
+
60
+ [tool.ruff.lint]
61
+ select = [
62
+ "E",
63
+ "W",
64
+ "F",
65
+ "I",
66
+ "C",
67
+ "B",
68
+ "PT",
69
+ "N",
70
+ "PL",
71
+ "RUF",
72
+ ]
73
+ ignore = [
74
+ "B008",
75
+ "C901",
76
+ "RET504",
77
+ "PLR2004",
78
+ ]
79
+
80
+ [tool.uv]
@@ -1,42 +1,32 @@
1
1
  [project]
2
2
  name = "diskstore"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "Simple MutableMapping compatible SQLite based disk storage."
5
5
  readme = "README.md"
6
+ license = "BSD-3-Clause"
7
+ license-files = ["LICENSE.txt"]
6
8
  requires-python = ">=3.10"
7
9
  authors = [{ name = "tds333", email = "tds333@mailbox.org" }]
8
- dependencies = [
9
- "apsw>=3.51.3",
10
- ]
10
+ dependencies = ["apsw>=3.51.3"]
11
11
 
12
12
  [dependency-groups]
13
13
  dev = [
14
+ "msgspec>=0.20.0",
15
+ "pydantic>=2.12.5,<=2.99.99b0",
14
16
  "pytest",
15
17
  "pytest-asyncio>=1.3.0",
16
18
  "pytest-cov",
17
19
  "pytest-examples>=0.0.18",
18
20
  "pytest-xdist",
19
- ]
20
- benchmark = [
21
- "diskcache",
22
- "ipython",
23
- "pickleDB<1",
24
- "pylibmc",
25
- "redis",
26
- "sqlitedict",
21
+ "structtype>=0.8.1",
27
22
  ]
28
23
  docs = [
29
- "mkdocs>=1.6.1",
30
24
  "mkdocstrings[python]>=1.0.3",
31
25
  "zensical>=0.0.23",
32
26
  ]
33
- ext-dev = [
34
- "msgspec>=0.20.0",
35
- "pydantic>=2.12.5",
36
- ]
37
27
 
38
28
  [build-system]
39
- requires = ["uv_build>=0.9.2,<0.12.0"]
29
+ requires = ["uv_build>=0.9.2,<1"]
40
30
  build-backend = "uv_build"
41
31
 
42
32
  [tool.coverage.run]
@@ -78,3 +68,6 @@ ignore = [
78
68
  "RET504",
79
69
  "PLR2004",
80
70
  ]
71
+
72
+ [tool.uv]
73
+ #exclude-newer = "1 days"
@@ -0,0 +1,275 @@
1
+ """DiskStore configuration classes and helpers for config."""
2
+
3
+ import dataclasses
4
+ import json
5
+ from abc import abstractmethod
6
+ from collections.abc import Sequence
7
+ from typing import Any, Iterable, Protocol
8
+
9
+ from .const import NO_DEFAULT, TIMEOUT, AnyLite, KeyType
10
+
11
+
12
+ def get_sqlite_type(type_) -> str:
13
+ if type_ in {"BLOB", "TEXT", "INTEGER", "REAL"}:
14
+ return type_
15
+ sqlite_type = "BLOB"
16
+ if type_ is str:
17
+ sqlite_type = "TEXT"
18
+ elif type_ is int:
19
+ sqlite_type = "INTEGER"
20
+ elif type_ is bool:
21
+ sqlite_type = "INTEGER"
22
+ elif type_ is float:
23
+ sqlite_type = "REAL"
24
+
25
+ return sqlite_type
26
+
27
+
28
+ def escape_name(name: str) -> str:
29
+ tablename = '"' + name.replace('"', '""') + '"'
30
+ return tablename
31
+
32
+
33
+ def is_bindable_default(value) -> bool:
34
+ """Whether *value* can be encoded by apsw as a SQL literal."""
35
+ return value is None or isinstance(value, (str, bytes, int, float))
36
+
37
+
38
+ class ConfigProtocol(Protocol):
39
+ """Configuration Protocol
40
+
41
+ Attributes:
42
+ tablename: Table name as string
43
+ key_type: key type as string or basic Python type like str, int, float
44
+ timeout: Timeout used to wait if someone blocks connections or with writes
45
+ pragmas: Dictionary with PRAGMAs to set when connections is initialized
46
+ fields: Iterable used for select, update and create statement with
47
+ field name, type, default.
48
+ ``[("value", str, NO_DEFAULT), ("value2", int, 0)]``
49
+
50
+ """
51
+
52
+ tablename: str
53
+ key_type: str
54
+ timeout: float
55
+ pragmas: dict
56
+ fields: Iterable
57
+ auto_migrate: bool
58
+
59
+ @abstractmethod
60
+ def dump_value(self, key: KeyType | None, value: Any) -> Sequence:
61
+ """Called with the key and value, should return a Sequence
62
+ with the key as first element, suitable as parameter tuple for
63
+ the INSERT/SET statement."""
64
+
65
+ @abstractmethod
66
+ def load_data(self, data: tuple) -> Any:
67
+ """load(db_data): called with the tuple selected from DB,
68
+ including the key at index 0. Should be converted to the
69
+ type normally received as value."""
70
+
71
+
72
+ class BaseConfig(ConfigProtocol):
73
+ """Base default configuratin."""
74
+
75
+ def __init__(
76
+ self,
77
+ *,
78
+ tablename=None,
79
+ key_type=None,
80
+ timeout=None,
81
+ pragmas=None,
82
+ auto_migrate=None,
83
+ ):
84
+ self.tablename = "DiskStore" if tablename is None else tablename
85
+ self.key_type = "BLOB" if key_type is None else get_sqlite_type(key_type)
86
+ self.timeout = TIMEOUT if timeout is None else float(timeout)
87
+ self.pragmas = {} if pragmas is None else pragmas
88
+ self.auto_migrate = True if auto_migrate is None else auto_migrate
89
+ self.fields = [("value", "BLOB", NO_DEFAULT)]
90
+
91
+ def dump_value(self, key: KeyType | None, value: Any) -> Sequence:
92
+ return (key, value)
93
+
94
+ def load_data(self, data: tuple) -> Any:
95
+ return data[1]
96
+
97
+
98
+ class NamedTupleConfig(BaseConfig):
99
+ def __init__( # noqa: PLR0913
100
+ self,
101
+ value_class,
102
+ tablename=None,
103
+ key_type=None,
104
+ timeout=None,
105
+ pragmas=None,
106
+ auto_migrate=None,
107
+ ):
108
+ tablename = value_class.__name__ if not tablename else tablename
109
+ super().__init__(
110
+ tablename=tablename,
111
+ key_type=key_type,
112
+ timeout=timeout,
113
+ pragmas=pragmas,
114
+ auto_migrate=auto_migrate,
115
+ )
116
+ self.value_class = value_class
117
+ self.fields = self.get_fields(value_class)
118
+
119
+ @staticmethod
120
+ def get_fields(value_class):
121
+ fields = []
122
+ value_columns = tuple(value_class._fields)
123
+ type_annotations = value_class.__annotations__
124
+ value_column_defaults: dict[str, AnyLite] = getattr(
125
+ value_class, "_field_defaults", {}
126
+ )
127
+ for name in value_columns:
128
+ type_cls = type_annotations.get(name, bytes) # types are optional
129
+ sqlite_type = get_sqlite_type(type_cls)
130
+ default = value_column_defaults.get(name, NO_DEFAULT)
131
+ if not is_bindable_default(default):
132
+ default = NO_DEFAULT
133
+ fields.append((name, sqlite_type, default))
134
+ if "_key" in value_columns:
135
+ raise ValueError(
136
+ f"Name _key is not allowed as attribute for {value_class},"
137
+ " listed as field name in _fields."
138
+ )
139
+
140
+ return tuple(fields)
141
+
142
+ def dump_value(self, key: KeyType | None, value: Any) -> Sequence:
143
+ return (key, *value)
144
+
145
+ def load_data(self, data: tuple) -> Any:
146
+ return self.value_class._make(data[1:])
147
+
148
+
149
+ class JsonConfig(BaseConfig):
150
+ def __init__(
151
+ self,
152
+ tablename=None,
153
+ key_type=None,
154
+ timeout=None,
155
+ pragmas=None,
156
+ auto_migrate=None,
157
+ ):
158
+ super().__init__(
159
+ tablename=tablename,
160
+ key_type=key_type,
161
+ timeout=timeout,
162
+ pragmas=pragmas,
163
+ auto_migrate=auto_migrate,
164
+ )
165
+ self.fields = (("value", "TEXT", NO_DEFAULT),)
166
+
167
+ def dump_value(self, key: KeyType | None, value: Any) -> Sequence:
168
+ return (key, json.dumps(value))
169
+
170
+ def load_data(self, data: tuple) -> Any:
171
+ return json.loads(data[1])
172
+
173
+
174
+ class DataclassConfig(BaseConfig):
175
+ def __init__( # noqa: PLR0913
176
+ self,
177
+ dataclass,
178
+ tablename=None,
179
+ key_type=None,
180
+ timeout=None,
181
+ pragmas=None,
182
+ auto_migrate=None,
183
+ ):
184
+ tablename = dataclass.__name__ if not tablename else tablename
185
+ super().__init__(
186
+ tablename=tablename,
187
+ key_type=key_type,
188
+ timeout=timeout,
189
+ pragmas=pragmas,
190
+ auto_migrate=auto_migrate,
191
+ )
192
+ self.dataclass = dataclass
193
+ self.fields = self.get_fields(dataclass)
194
+
195
+ @staticmethod
196
+ def get_fields(dataclass):
197
+ if not dataclasses.is_dataclass(dataclass):
198
+ raise ValueError("It is not a dataclass.")
199
+ dc_fields = dataclasses.fields(dataclass)
200
+ value_columns = tuple(field.name for field in dc_fields)
201
+ if "_key" in value_columns:
202
+ raise ValueError("Name _key is not allowed as attribute for dataclass.")
203
+ type_annotations = getattr(dataclass, "__annotations__", {}) or {}
204
+ fields = []
205
+ for f in dc_fields:
206
+ type_cls = type_annotations.get(f.name, bytes)
207
+ sqlite_type = get_sqlite_type(type_cls)
208
+ if f.default is not dataclasses.MISSING and is_bindable_default(f.default):
209
+ default = f.default
210
+ else:
211
+ default = NO_DEFAULT
212
+ fields.append((f.name, sqlite_type, default))
213
+ return tuple(fields)
214
+
215
+ def dump_value(self, key: KeyType | None, value: Any) -> Sequence:
216
+ return (key, *dataclasses.astuple(value))
217
+
218
+ def load_data(self, data: tuple) -> Any:
219
+ return self.dataclass(*data[1:])
220
+
221
+
222
+ class PydanticConfig(BaseConfig):
223
+ def __init__( # noqa: PLR0913
224
+ self,
225
+ model,
226
+ tablename=None,
227
+ key_type=None,
228
+ timeout=None,
229
+ pragmas=None,
230
+ auto_migrate=None,
231
+ ):
232
+ tablename = model.__name__ if not tablename else tablename
233
+ super().__init__(
234
+ tablename=tablename,
235
+ key_type=key_type,
236
+ timeout=timeout,
237
+ pragmas=pragmas,
238
+ auto_migrate=auto_migrate,
239
+ )
240
+ self.fields = (("value", "TEXT", NO_DEFAULT),)
241
+ self.model = model
242
+
243
+ def dump_value(self, key: KeyType | None, value: Any) -> Sequence:
244
+ return (key, value.model_dump_json())
245
+
246
+ def load_data(self, data: tuple) -> Any:
247
+ return self.model.model_validate_json(data[1])
248
+
249
+
250
+ class StructtypeConfig(BaseConfig):
251
+ def __init__( # noqa: PLR0913, PLR0917
252
+ self,
253
+ struct,
254
+ tablename=None,
255
+ key_type=None,
256
+ timeout=None,
257
+ pragmas=None,
258
+ auto_migrate=None,
259
+ ):
260
+ tablename = struct.__name__ if not tablename else tablename
261
+ super().__init__(
262
+ tablename=tablename,
263
+ key_type=key_type,
264
+ timeout=timeout,
265
+ pragmas=pragmas,
266
+ auto_migrate=auto_migrate,
267
+ )
268
+ self.fields = (("value", "BLOB", NO_DEFAULT),)
269
+ self.struct = struct
270
+
271
+ def dump_value(self, key: KeyType | None, value: Any) -> Sequence:
272
+ return (key, value.struct_dump_json())
273
+
274
+ def load_data(self, data: tuple) -> Any:
275
+ return self.struct.struct_validate_json(data[1])
@@ -5,19 +5,31 @@ from typing import (
5
5
  Union,
6
6
  )
7
7
 
8
- TIMEOUT = 60.0
8
+ TIMEOUT = 10.0
9
9
  "default timout in seconds"
10
10
 
11
11
  MISSING = object()
12
12
 
13
13
 
14
+ class NO_DEFAULT: # noqa: N801
15
+ """Sentinel marker for fields without a default value."""
16
+
17
+
18
+ DEFAULT_RO_PRAGMAS = {
19
+ "cache_size": 2**13, # 8,192 pages
20
+ "mmap_size": 2**28, # 256 MB
21
+ "temp_store": 2, # 0=DEFAULT, 1=FILE, 2=MEMORY
22
+ "synchronous": 1, # 0=OFF, 1=NORMAL, 2=FULL, 3=EXTRA
23
+ }
24
+ "default read only pragma settings"
25
+
14
26
  DEFAULT_PRAGMAS = {
15
27
  "auto_vacuum": 0, # 1=FULL, 0=None
16
28
  "cache_size": 2**13, # 8,192 pages
17
29
  "journal_mode": "wal",
18
30
  "mmap_size": 2**28, # 256 MB
19
31
  "synchronous": 1, # 0=OFF, 1=NORMAL, 2=FULL, 3=EXTRA
20
- "temp_store": "memory",
32
+ "temp_store": 2, # 0=DEFAULT, 1=FILE, 2=MEMORY
21
33
  # "pargma_busy_timeout": 5000, # milliseconds
22
34
  }
23
35
  "default pragma settings"