fromscratch-pykv 0.4.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,108 @@
1
+ Metadata-Version: 2.5
2
+ Name: fromscratch-pykv
3
+ Version: 0.4.0
4
+ Summary: An educational key-value store library built from scratch in Python
5
+ Project-URL: Homepage, https://github.com/robcox2910/pykv
6
+ Project-URL: Repository, https://github.com/robcox2910/pykv
7
+ Project-URL: Issues, https://github.com/robcox2910/pykv/issues
8
+ Project-URL: Documentation, https://robcox2910.github.io/pykv/
9
+ Author: Rob Cox
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: cache,education,key-value,store
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Education
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.14
22
+ Provides-Extra: dev
23
+ Requires-Dist: commitizen>=4.4.0; extra == 'dev'
24
+ Requires-Dist: hypothesis>=6.170; extra == 'dev'
25
+ Requires-Dist: pip-audit>=2.10.0; extra == 'dev'
26
+ Requires-Dist: pre-commit>=4.0.1; extra == 'dev'
27
+ Requires-Dist: pyright>=1.1.400; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.3.4; extra == 'dev'
30
+ Requires-Dist: ruff>=0.9.3; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # PyKV
34
+
35
+ An educational key-value store built from scratch in Python.
36
+
37
+ Part of the [PyLearn](https://github.com/robcox2910) series — learning computer science by building real tools.
38
+
39
+ ## What Is a Key-Value Store?
40
+
41
+ Think of a **phonebook**: you look up a name (the *key*) and get back a phone number (the *value*). A key-value store works the same way — it's the simplest kind of database.
42
+
43
+ ## Features
44
+
45
+ - **Get / Set / Delete** — basic phonebook operations
46
+ - **TTL (Time-To-Live)** — keys that expire, like milk with a use-by date
47
+ - **Multiple Data Types** — store strings, numbers, lists, and dicts
48
+ - **Persistence** — save to disk so data survives a restart
49
+ - **Namespaces** — group keys like folders in a filing cabinet
50
+ - **Batch Operations** — handle many keys at once, like a grocery list
51
+ - **Atomic Counters** — thread-safe increment/decrement, like a tally counter
52
+ - **Pattern Matching** — find keys matching a glob pattern
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ from pykv.store import BasicStore
58
+
59
+ store = BasicStore()
60
+ store.set("greeting", "hello")
61
+ print(store.get("greeting")) # "hello"
62
+ ```
63
+
64
+ ## Releasing
65
+
66
+ Other projects in the series ([PyStack](https://github.com/robcox2910/pystack)) use a pinned release of this one,
67
+ not whatever is on `main`. To publish a change for them:
68
+
69
+ 1. Merge the change to `main`.
70
+ 2. Tag it: `git tag -a vX.Y.Z -m vX.Y.Z && git push origin vX.Y.Z`.
71
+ A change to the public API bumps the minor version (`v0.1.0` → `v0.2.0`);
72
+ anything else bumps the patch (`v0.1.0` → `v0.1.1`).
73
+ 3. In each project that uses it, edit the tag in `[tool.uv.sources]`, run
74
+ `uv lock --upgrade-package <name>`, and open a pull request so CI checks
75
+ the pair together.
76
+
77
+ <!-- series:start -->
78
+ ## Related Projects
79
+
80
+ This project is one of a series that builds real systems "from scratch in Python" to teach how they work. See the [series home](https://robcox2910.github.io/) for the full story.
81
+
82
+ | Project | What it teaches | Repository | Docs |
83
+ | --- | --- | --- | --- |
84
+ | PyOS | Operating systems | [py-os](https://github.com/robcox2910/py-os) | [Docs](https://robcox2910.github.io/py-os/) |
85
+ | Pebble | Compilers and programming languages | [pebble-lang](https://github.com/robcox2910/pebble-lang) | [Docs](https://robcox2910.github.io/pebble-lang/) |
86
+ | PyDB | Relational databases | [pydb](https://github.com/robcox2910/pydb) | [Docs](https://robcox2910.github.io/pydb/) |
87
+ | **PyKV** | **Key-value stores** | [pykv](https://github.com/robcox2910/pykv) | [Docs](https://robcox2910.github.io/pykv/) |
88
+ | PyDocDB | Document databases | [pydoc-db](https://github.com/robcox2910/pydoc-db) | [Docs](https://robcox2910.github.io/pydoc-db/) |
89
+ | PyGraphDB | Graph databases | [pygraphdb](https://github.com/robcox2910/pygraphdb) | [Docs](https://robcox2910.github.io/pygraphdb/) |
90
+ | PyTSDB | Time-series databases | [pytsdb](https://github.com/robcox2910/pytsdb) | [Docs](https://robcox2910.github.io/pytsdb/) |
91
+ | PyVecDB | Vector databases | [pyvecdb](https://github.com/robcox2910/pyvecdb) | [Docs](https://robcox2910.github.io/pyvecdb/) |
92
+ | PyColDB | Column-family databases | [pycoldb](https://github.com/robcox2910/pycoldb) | [Docs](https://robcox2910.github.io/pycoldb/) |
93
+ | PyCrypt | Cryptography | [pycrypt](https://github.com/robcox2910/pycrypt) | [Docs](https://robcox2910.github.io/pycrypt/) |
94
+ | PyNet | Computer networking | [pynet](https://github.com/robcox2910/pynet) | [Docs](https://robcox2910.github.io/pynet/) |
95
+ | PyWeb | HTTP web servers | [pyweb](https://github.com/robcox2910/pyweb) | [Docs](https://robcox2910.github.io/pyweb/) |
96
+ | PyGit | Version control | [pygit](https://github.com/robcox2910/pygit) | [Docs](https://robcox2910.github.io/pygit/) |
97
+ | PySearch | Search engines | [pysearch](https://github.com/robcox2910/pysearch) | [Docs](https://robcox2910.github.io/pysearch/) |
98
+ | PyMQ | Message queues | [pymq](https://github.com/robcox2910/pymq) | [Docs](https://robcox2910.github.io/pymq/) |
99
+ | PyLLM | Language models (the brain) | [pyllm](https://github.com/robcox2910/pyllm) | [Docs](https://robcox2910.github.io/pyllm/) |
100
+ | PyScheduler | Job scheduling | [pyscheduler](https://github.com/robcox2910/pyscheduler) | [Docs](https://robcox2910.github.io/pyscheduler/) |
101
+ | PyStack | Full-stack integration | [pystack](https://github.com/robcox2910/pystack) | [Docs](https://robcox2910.github.io/pystack/) |
102
+ <!-- series:end -->
103
+
104
+ <!-- docs:start -->
105
+ ## Documentation
106
+
107
+ Full docs (with kid-friendly explanations of every idea): https://robcox2910.github.io/pykv/
108
+ <!-- docs:end -->
@@ -0,0 +1,17 @@
1
+ pykv/__init__.py,sha256=TpQ1KNWdTqPxMx0BJ6CcCMN-GLfEdSN7O3W_wRrSdPo,2025
2
+ pykv/_version.py,sha256=HGBlVvZpQ8N2kh1n_LY5PM4s9ljcIgdbOCgXN-uFje4,520
3
+ pykv/atomic.py,sha256=QLLPGZqpA-TgVV5lDEKCLB7vVr05bxj97_Fwykz_gi4,2345
4
+ pykv/batch.py,sha256=LKMVD5CZ1FTNsssbSX6sGNGT5rHIKB4mflVTa3q2hS8,3176
5
+ pykv/counters.py,sha256=4nrcG04kGJu3kS24nyOQHXdsXUk1F1hnoKVCnqoP6j0,4087
6
+ pykv/errors.py,sha256=6VJJ2eHHPcB_8dDJDVbbbiZmvzfjvzrZGHbXjfPG7TQ,2187
7
+ pykv/namespace.py,sha256=27ljO5mL7RDdPFRhTgDmyQslPCtUJrkw_QjQfft15Ss,7807
8
+ pykv/patterns.py,sha256=RnEepUDjdsYG72r2zwuAQX8jL73yTu7vvOMl2C7koUk,2463
9
+ pykv/persistence.py,sha256=I5yzHMiWkbWnXVfkZmz7NUP7pSap8AgSFCXYnLsj_90,4984
10
+ pykv/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ pykv/store.py,sha256=jW-1tLO-vMZ8-t5YlX9gDQezVPDMoRsmRGiDLcMlmAQ,5094
12
+ pykv/ttl.py,sha256=-S2nIfBJwSen1H5NQVmlmVa2ATELkbLKrj3k_Pcih9w,8286
13
+ pykv/types.py,sha256=DV_kXwZFpHsskAzyAP2BhqBUOgPBHcSLiaQy4vx3q0c,3311
14
+ fromscratch_pykv-0.4.0.dist-info/METADATA,sha256=VGEh_EZJUoal3HywhiRkvbYy0uUu0j8StKPD3CR3SDE,5820
15
+ fromscratch_pykv-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
16
+ fromscratch_pykv-0.4.0.dist-info/licenses/LICENSE,sha256=NtHM2ak--oE71VCcY50kcCoC1cKS0LRUsxq_xA89RNc,1064
17
+ fromscratch_pykv-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rob Cox
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.
pykv/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ """PyKV -- an educational key-value store built from scratch in Python.
2
+
3
+ A key-value store is like a giant phonebook for your data. You give each
4
+ piece of information a unique name (the "key") and store some data (the
5
+ "value") next to it. Later, you can look up the data just by knowing
6
+ its name -- lightning fast!
7
+
8
+ Real-world key-value stores like Redis and Memcached power some of the
9
+ biggest websites in the world. PyKV teaches you how they work from the
10
+ inside, one piece at a time.
11
+
12
+ Quick start:
13
+ >>> from pykv import BasicStore
14
+ >>> store = BasicStore()
15
+ >>> store.set("greeting", "Hello, world!")
16
+ >>> store.get("greeting")
17
+ 'Hello, world!'
18
+
19
+ Features:
20
+ - BasicStore: Simple get/set/delete operations
21
+ - TTLStore: Keys that expire after a time limit
22
+ - Namespace: Group keys with prefixes
23
+ - AtomicCounter: Thread-safe counters
24
+ - Persistence: Save and load from disk
25
+ - Batch operations: Work with many keys at once
26
+ - Pattern matching: Find keys with wildcards
27
+ """
28
+
29
+ from pykv.batch import batch_delete, batch_exists, batch_get, batch_set
30
+ from pykv.counters import AtomicCounter
31
+ from pykv.errors import (
32
+ KeyExpiredError,
33
+ KeyNotFoundError,
34
+ NamespaceError,
35
+ PersistenceError,
36
+ PyKVError,
37
+ SerializationError,
38
+ )
39
+ from pykv.namespace import Namespace
40
+ from pykv.patterns import count_matches, match_items, match_keys
41
+ from pykv.persistence import load_store, save_store
42
+ from pykv.store import BasicStore
43
+ from pykv.ttl import TTLStore
44
+ from pykv.types import StorableValue
45
+
46
+ __all__ = [
47
+ "AtomicCounter",
48
+ # Core
49
+ "BasicStore",
50
+ "KeyExpiredError",
51
+ "KeyNotFoundError",
52
+ # Features
53
+ "Namespace",
54
+ "NamespaceError",
55
+ "PersistenceError",
56
+ # Errors
57
+ "PyKVError",
58
+ "SerializationError",
59
+ "StorableValue",
60
+ "TTLStore",
61
+ "batch_delete",
62
+ "batch_exists",
63
+ "batch_get",
64
+ # Batch
65
+ "batch_set",
66
+ "count_matches",
67
+ "load_store",
68
+ "match_items",
69
+ # Patterns
70
+ "match_keys",
71
+ # Persistence
72
+ "save_store",
73
+ ]
pykv/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.4.0'
22
+ __version_tuple__ = version_tuple = (0, 4, 0)
23
+
24
+ __commit_id__ = commit_id = None
pykv/atomic.py ADDED
@@ -0,0 +1,66 @@
1
+ """Safe saving -- never leave a half-written file behind.
2
+
3
+ The usual way to save a file is: open it, wipe it, write the new
4
+ contents. If the computer crashes after the wipe but before the writing
5
+ finishes, your data is gone -- the old version was wiped and the new one
6
+ never arrived.
7
+
8
+ So we save like a careful student copying out their homework:
9
+
10
+ 1. Write the new version on a **fresh sheet** (a temporary file sitting
11
+ right next to the real one).
12
+ 2. Make sure the **ink is dry** -- ``fsync`` forces the bytes out of the
13
+ computer's memory and onto the disk.
14
+ 3. **Swap** the fresh sheet in for the old one in a single instant step
15
+ (``Path.replace``). There is never a moment with no complete file: you
16
+ see either the old version or the new one, never half of each.
17
+
18
+ Real databases (SQLite, PostgreSQL, Redis) all use this same trick.
19
+ """
20
+
21
+ import contextlib
22
+ import os
23
+ import stat
24
+ import tempfile
25
+ from pathlib import Path
26
+
27
+ # Permissions for a brand-new file: owner can read/write, others can read.
28
+ _NEW_FILE_MODE = 0o644
29
+
30
+
31
+ def atomic_write_text(path: Path, text: str) -> None:
32
+ """Replace the file at *path* with *text*, so a crash can never leave it half-written.
33
+
34
+ Args:
35
+ path: The file to create or replace (its folder must already exist).
36
+ text: The new contents, saved as UTF-8.
37
+
38
+ """
39
+ mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else _NEW_FILE_MODE
40
+ fd, temp_name = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
41
+ temp_path = Path(temp_name)
42
+ try:
43
+ with os.fdopen(fd, "w", encoding="utf-8") as temp_file:
44
+ temp_file.write(text)
45
+ temp_file.flush()
46
+ os.fsync(temp_file.fileno())
47
+ temp_path.chmod(mode)
48
+ temp_path.replace(path)
49
+ except BaseException:
50
+ temp_path.unlink(missing_ok=True) # don't leave the scrap sheet lying around
51
+ raise
52
+ _sync_folder(path.parent)
53
+
54
+
55
+ def _sync_folder(folder: Path) -> None:
56
+ """Make the swap itself durable by syncing the folder's list of files.
57
+
58
+ Some systems (like Windows) can't sync a folder; there the swap is
59
+ still safe, just not guaranteed to survive a power cut.
60
+ """
61
+ with contextlib.suppress(OSError):
62
+ fd = os.open(folder, os.O_RDONLY)
63
+ try:
64
+ os.fsync(fd)
65
+ finally:
66
+ os.close(fd)
pykv/batch.py ADDED
@@ -0,0 +1,110 @@
1
+ """Batch operations -- get and set multiple keys at once.
2
+
3
+ Imagine going to the grocery store. You could make one trip for each item
4
+ on your list -- milk, bread, eggs -- but that would be exhausting! Instead,
5
+ you take your whole shopping list and grab everything in one trip. Batch
6
+ operations work the same way: instead of calling get() or set() over and
7
+ over, you do it all at once.
8
+
9
+ This is more convenient -- one call instead of many -- especially when you
10
+ have lots of keys to work with. Under the hood it is still a plain loop
11
+ calling ``set`` or ``get`` one key at a time, so it is not actually faster
12
+ than doing it yourself; it just keeps your code short and tidy.
13
+
14
+ Example:
15
+ >>> store = BasicStore()
16
+ >>> batch_set(store, {"name": "Alice", "age": 13, "grade": 8})
17
+ >>> batch_get(store, ["name", "age"])
18
+ {'name': 'Alice', 'age': 13}
19
+
20
+ """
21
+
22
+ from pykv.store import BasicStore
23
+ from pykv.types import StorableValue
24
+
25
+
26
+ def batch_set(store: BasicStore, items: dict[str, StorableValue]) -> None:
27
+ """Set multiple key-value pairs in one call.
28
+
29
+ Like dropping off your whole grocery bag at once instead of
30
+ putting items away one by one.
31
+
32
+ Args:
33
+ store: The store to write to.
34
+ items: A dictionary of key-value pairs to set.
35
+
36
+ """
37
+ for key, value in items.items():
38
+ store.set(key, value)
39
+
40
+
41
+ def batch_get(
42
+ store: BasicStore,
43
+ keys: list[str],
44
+ *,
45
+ skip_missing: bool = False,
46
+ ) -> dict[str, StorableValue]:
47
+ """Retrieve multiple values in one call.
48
+
49
+ Like reading multiple entries from your phonebook at once.
50
+ If ``skip_missing`` is True, keys that do not exist are silently
51
+ skipped. Otherwise, a ``KeyNotFoundError`` is raised for any
52
+ missing key.
53
+
54
+ Args:
55
+ store: The store to read from.
56
+ keys: A list of keys to retrieve.
57
+ skip_missing: If True, skip keys that do not exist instead of raising.
58
+
59
+ Returns:
60
+ A dictionary of the found key-value pairs.
61
+
62
+ Raises:
63
+ KeyNotFoundError: If a key is missing and skip_missing is False.
64
+
65
+ """
66
+ result: dict[str, StorableValue] = {}
67
+ for key in keys:
68
+ if skip_missing:
69
+ if store.exists(key):
70
+ result[key] = store.get(key)
71
+ else:
72
+ result[key] = store.get(key)
73
+ return result
74
+
75
+
76
+ def batch_delete(store: BasicStore, keys: list[str]) -> int:
77
+ """Delete multiple keys in one call.
78
+
79
+ Like erasing several entries from your phonebook at once.
80
+
81
+ Args:
82
+ store: The store to delete from.
83
+ keys: A list of keys to remove.
84
+
85
+ Returns:
86
+ The number of keys that were actually deleted.
87
+
88
+ """
89
+ count = 0
90
+ for key in keys:
91
+ if store.delete(key):
92
+ count += 1
93
+ return count
94
+
95
+
96
+ def batch_exists(store: BasicStore, keys: list[str]) -> dict[str, bool]:
97
+ """Check whether multiple keys exist in one call.
98
+
99
+ Like scanning your phonebook for a list of names and marking
100
+ which ones you found.
101
+
102
+ Args:
103
+ store: The store to check.
104
+ keys: A list of keys to look for.
105
+
106
+ Returns:
107
+ A dictionary mapping each key to True/False.
108
+
109
+ """
110
+ return {key: store.exists(key) for key in keys}
pykv/counters.py ADDED
@@ -0,0 +1,137 @@
1
+ """Atomic counters -- thread-safe increment and decrement.
2
+
3
+ Imagine a tally counter (the clicky thing a bouncer uses to count people
4
+ entering a club). Every time someone walks in, you click +1. Every time
5
+ someone leaves, you click -1. The key word is "atomic" -- each click is
6
+ one single, unbreakable action. Even if two people try to click at the
7
+ exact same time, the counter never gets confused.
8
+
9
+ In programming, this matters when multiple threads (like multiple workers)
10
+ try to update the same number at the same time. Without atomic operations,
11
+ two threads reading "5" at the same time might both write "6" instead of
12
+ one writing "6" and the other writing "7".
13
+
14
+ Example:
15
+ >>> store = BasicStore()
16
+ >>> counter = AtomicCounter(store)
17
+ >>> counter.increment("visitors")
18
+ 1
19
+ >>> counter.increment("visitors")
20
+ 2
21
+ >>> counter.decrement("visitors")
22
+ 1
23
+
24
+ """
25
+
26
+ import threading
27
+
28
+ from pykv.errors import KeyNotFoundError
29
+ from pykv.store import BasicStore
30
+ from pykv.types import StorableValue
31
+
32
+
33
+ class AtomicCounter:
34
+ """Thread-safe counter operations on integer keys in a BasicStore.
35
+
36
+ Like a digital tally counter that multiple people can click at the
37
+ same time without it ever getting confused. Each click (increment
38
+ or decrement) is guaranteed to be accurate.
39
+ """
40
+
41
+ def __init__(self, store: BasicStore) -> None:
42
+ """Create an atomic counter backed by the given store.
43
+
44
+ Args:
45
+ store: The BasicStore to use for storing counter values.
46
+
47
+ """
48
+ self._store = store
49
+ self._lock = threading.Lock()
50
+
51
+ def increment(self, key: str, amount: int = 1) -> int:
52
+ """Increase a counter by the given amount, creating it if needed.
53
+
54
+ Like clicking the tally counter one or more times. If the counter
55
+ does not exist yet, it starts at 0 before incrementing.
56
+
57
+ Args:
58
+ key: The counter name.
59
+ amount: How much to add (default 1).
60
+
61
+ Returns:
62
+ The new counter value after incrementing.
63
+
64
+ """
65
+ with self._lock:
66
+ current = self._get_int(key)
67
+ new_value = current + amount
68
+ self._store.set(key, new_value)
69
+ return new_value
70
+
71
+ def decrement(self, key: str, amount: int = 1) -> int:
72
+ """Decrease a counter by the given amount, creating it if needed.
73
+
74
+ Like un-clicking the tally counter. The counter can go negative
75
+ (below zero) -- there is no floor.
76
+
77
+ Args:
78
+ key: The counter name.
79
+ amount: How much to subtract (default 1).
80
+
81
+ Returns:
82
+ The new counter value after decrementing.
83
+
84
+ """
85
+ with self._lock:
86
+ current = self._get_int(key)
87
+ new_value = current - amount
88
+ self._store.set(key, new_value)
89
+ return new_value
90
+
91
+ def get_value(self, key: str) -> int:
92
+ """Read the current counter value.
93
+
94
+ Like glancing at the number on the tally counter without clicking
95
+ it. If nobody has clicked it yet, it reads 0.
96
+
97
+ Args:
98
+ key: The counter name.
99
+
100
+ Returns:
101
+ The current integer value, or 0 if the counter does not exist.
102
+
103
+ """
104
+ with self._lock:
105
+ return self._get_int(key)
106
+
107
+ def reset(self, key: str) -> None:
108
+ """Reset a counter to zero.
109
+
110
+ Like clearing the tally counter back to 0.
111
+
112
+ Args:
113
+ key: The counter name to reset.
114
+
115
+ """
116
+ with self._lock:
117
+ self._store.set(key, 0)
118
+
119
+ def _get_int(self, key: str) -> int:
120
+ """Read the current integer value of a key, defaulting to 0.
121
+
122
+ Args:
123
+ key: The key to read.
124
+
125
+ Returns:
126
+ The integer value, or 0 if the key does not exist.
127
+
128
+ """
129
+ try:
130
+ value: StorableValue = self._store.get(key)
131
+ except KeyNotFoundError:
132
+ return 0
133
+
134
+ if not isinstance(value, int):
135
+ msg = f"Counter key {key!r} holds a {type(value).__name__}, not an int"
136
+ raise TypeError(msg)
137
+ return value
pykv/errors.py ADDED
@@ -0,0 +1,68 @@
1
+ """Custom exceptions for PyKV.
2
+
3
+ Think of exceptions like warning signs on a road. They tell you when something
4
+ goes wrong so you can handle it properly instead of crashing. Each exception
5
+ type is like a different road sign -- "Key Not Found" is like "Road Closed",
6
+ while "Type Error" is like "Wrong Way".
7
+ """
8
+
9
+
10
+ class PyKVError(Exception):
11
+ """Base exception for all PyKV errors.
12
+
13
+ Every PyKV error inherits from this class, like how every animal
14
+ inherits from the "Animal" category. This lets you catch ALL PyKV
15
+ errors with a single ``except PyKVError`` block.
16
+ """
17
+
18
+
19
+ class KeyNotFoundError(PyKVError):
20
+ """Raise when a requested key does not exist in the store.
21
+
22
+ Imagine looking up a word in a dictionary and it is not there.
23
+ That is what this error represents.
24
+ """
25
+
26
+ def __init__(self, key: str) -> None:
27
+ """Initialize with the missing key name."""
28
+ self.key = key
29
+ super().__init__(f"Key not found: {key!r}")
30
+
31
+
32
+ class KeyExpiredError(PyKVError):
33
+ """Raise when a key has expired due to its TTL.
34
+
35
+ Like trying to drink milk after the expiration date -- the key
36
+ existed once, but its time-to-live has passed.
37
+ """
38
+
39
+ def __init__(self, key: str) -> None:
40
+ """Initialize with the expired key name."""
41
+ self.key = key
42
+ super().__init__(f"Key has expired: {key!r}")
43
+
44
+
45
+ class SerializationError(PyKVError):
46
+ """Raise when a value cannot be serialized or deserialized.
47
+
48
+ Some things cannot be written down in a notebook. If you try to
49
+ store something that PyKV cannot convert to a saveable format,
50
+ you get this error.
51
+ """
52
+
53
+
54
+ class PersistenceError(PyKVError):
55
+ """Raise when saving to or loading from disk fails.
56
+
57
+ Like trying to save your game but the memory card is full or
58
+ broken. Something went wrong with the file on disk.
59
+ """
60
+
61
+
62
+ class NamespaceError(PyKVError):
63
+ """Raise when a namespace name or a key inside a namespace breaks the rules.
64
+
65
+ Folders in a filing cabinet must never share a paper. A namespace
66
+ name can't be empty, and a key inside a namespace can't contain
67
+ the ``:`` separator -- otherwise one folder could reach into another.
68
+ """