Skip to content

Commit 116a0c7

Browse files
authored
Add Windows Registry store for key-value storage (#73)
1 parent 6a409c6 commit 116a0c7

File tree

12 files changed

+331
-5
lines changed

12 files changed

+331
-5
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pip install py-key-value-aio[memory]
3737
pip install py-key-value-aio[disk]
3838
pip install py-key-value-aio[dynamodb]
3939
pip install py-key-value-aio[elasticsearch]
40-
# or: redis, mongodb, memcached, valkey, vault, rocksdb, see below for all options
40+
# or: redis, mongodb, memcached, valkey, vault, registry, rocksdb, see below for all options
4141
```
4242

4343
```python
@@ -98,11 +98,12 @@ Local stores are stored in memory or on disk, local to the application.
9898
| Memory ||| `MemoryStore()` |
9999
| Disk | ☑️ || `DiskStore(directory="./cache")` |
100100
| Disk (Per-Collection) | ☑️ || `MultiDiskStore(directory="./cache")` |
101+
| Null (test) ||| `NullStore()` |
101102
| RocksDB | ☑️ || `RocksDBStore(path="./rocksdb")` |
102103
| Simple (test) ||| `SimpleStore()` |
103-
| Null (test) | || `NullStore()` |
104+
| Windows Registry | ☑️ | | `WindowsRegistryStore(hive="HKEY_CURRENT_USER", registry_path="Software\\py-key-value")` |
104105

105-
#### Secret stores
106+
#### Local - Secret stores
106107
Secret stores are stores that are used to store sensitive data, typically in an Operating System's secret store.
107108

108109
| Secret Stores | Async | Sync | Example |

key-value/key-value-aio/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ elasticsearch = ["elasticsearch>=9.0.0", "aiohttp>=3.12"]
4343
dynamodb = ["aioboto3>=13.3.0", "types-aiobotocore-dynamodb>=2.16.0"]
4444
keyring = ["keyring>=25.6.0"]
4545
keyring-linux = ["keyring>=25.6.0", "dbus-python>=1.4.0"]
46+
registry = [] # Uses built-in winreg module on Windows
4647
pydantic = ["pydantic>=2.11.9"]
4748
rocksdb = ["rocksdict>=0.3.0"]
4849
wrappers-encryption = ["cryptography>=45.0.0"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from key_value.aio.stores.windows_registry.store import WindowsRegistryStore
2+
3+
__all__ = ["WindowsRegistryStore"]
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Windows Registry-based key-value store."""
2+
3+
from typing import Literal
4+
from winreg import HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE
5+
6+
from key_value.shared.utils.managed_entry import ManagedEntry
7+
from key_value.shared.utils.sanitize import ALPHANUMERIC_CHARACTERS, sanitize_string
8+
from typing_extensions import override
9+
10+
from key_value.aio.stores.base import BaseStore
11+
12+
try:
13+
import winreg
14+
except ImportError as e:
15+
msg = "WindowsRegistryStore requires Windows platform (winreg module)"
16+
raise ImportError(msg) from e
17+
18+
MAX_KEY_LENGTH = 96
19+
ALLOWED_KEY_CHARACTERS: str = ALPHANUMERIC_CHARACTERS
20+
21+
MAX_COLLECTION_LENGTH = 96
22+
ALLOWED_COLLECTION_CHARACTERS: str = ALPHANUMERIC_CHARACTERS
23+
DEFAULT_REGISTRY_PATH = "Software\\py-key-value"
24+
DEFAULT_HIVE = "HKEY_CURRENT_USER"
25+
26+
27+
class WindowsRegistryStore(BaseStore):
28+
"""Windows Registry-based key-value store.
29+
30+
This store uses the Windows Registry to persist key-value pairs. Each entry is stored
31+
as a string value in the registry under HKEY_CURRENT_USER\\Software\\{root}\\{collection}\\{key}.
32+
33+
Note: TTL is not natively supported by Windows Registry, so TTL information is stored
34+
within the JSON payload and checked at retrieval time.
35+
"""
36+
37+
def __init__(
38+
self,
39+
*,
40+
hive: Literal["HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE"] | None = None,
41+
registry_path: str | None = None,
42+
default_collection: str | None = None,
43+
) -> None:
44+
"""Initialize the Windows Registry store.
45+
46+
Args:
47+
hive: The hive to use. Defaults to "HKEY_CURRENT_USER".
48+
registry_path: The registry path to use. Must be a valid registry path under the hive. Defaults to "Software\\py-key-value".
49+
default_collection: The default collection to use if no collection is provided.
50+
"""
51+
self._hive = HKEY_LOCAL_MACHINE if hive == "HKEY_LOCAL_MACHINE" else HKEY_CURRENT_USER
52+
self._registry_path = registry_path or DEFAULT_REGISTRY_PATH
53+
54+
super().__init__(default_collection=default_collection)
55+
56+
def _sanitize_collection_name(self, collection: str) -> str:
57+
return sanitize_string(
58+
value=collection,
59+
max_length=MAX_COLLECTION_LENGTH,
60+
allowed_characters=ALLOWED_COLLECTION_CHARACTERS,
61+
)
62+
63+
def _sanitize_key(self, key: str) -> str:
64+
return sanitize_string(
65+
value=key,
66+
max_length=MAX_KEY_LENGTH,
67+
allowed_characters=ALLOWED_KEY_CHARACTERS,
68+
)
69+
70+
def _get_registry_path(self, *, collection: str) -> str:
71+
"""Get the full registry path for a collection."""
72+
sanitized_collection = self._sanitize_collection_name(collection=collection)
73+
return f"{self._registry_path}\\{sanitized_collection}"
74+
75+
@override
76+
async def _get_managed_entry(self, *, key: str, collection: str) -> ManagedEntry | None:
77+
sanitized_key = self._sanitize_key(key=key)
78+
registry_path = self._get_registry_path(collection=collection)
79+
80+
try:
81+
with winreg.OpenKey(key=self._hive, sub_key=registry_path, reserved=0, access=winreg.KEY_READ) as reg_key:
82+
json_str, _ = winreg.QueryValueEx(reg_key, sanitized_key)
83+
except (FileNotFoundError, OSError):
84+
return None
85+
86+
if json_str is None:
87+
return None
88+
89+
return ManagedEntry.from_json(json_str=json_str)
90+
91+
@override
92+
async def _put_managed_entry(self, *, key: str, collection: str, managed_entry: ManagedEntry) -> None:
93+
sanitized_key = self._sanitize_key(key=key)
94+
registry_path = self._get_registry_path(collection=collection)
95+
96+
json_str: str = managed_entry.to_json()
97+
98+
try:
99+
with winreg.CreateKey(self._hive, registry_path) as reg_key:
100+
winreg.SetValueEx(reg_key, sanitized_key, 0, winreg.REG_SZ, json_str)
101+
except OSError as e:
102+
msg = f"Failed to write to registry: {e}"
103+
raise RuntimeError(msg) from e
104+
105+
@override
106+
async def _delete_managed_entry(self, *, key: str, collection: str) -> bool:
107+
sanitized_key = self._sanitize_key(key=key)
108+
registry_path = self._get_registry_path(collection=collection)
109+
110+
try:
111+
with winreg.OpenKey(self._hive, registry_path, 0, winreg.KEY_WRITE) as reg_key:
112+
winreg.DeleteValue(reg_key, sanitized_key)
113+
except (FileNotFoundError, OSError):
114+
return False
115+
else:
116+
return True

key-value/key-value-aio/tests/stores/windows_registry/__init__.py

Whitespace-only changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing import TYPE_CHECKING, Any
2+
3+
import pytest
4+
from key_value.shared_test.cases import LARGE_TEST_DATA_ARGNAMES, LARGE_TEST_DATA_ARGVALUES, LARGE_TEST_DATA_IDS
5+
from typing_extensions import override
6+
7+
from key_value.aio.stores.base import BaseStore
8+
from tests.conftest import detect_on_windows
9+
from tests.stores.base import BaseStoreTests
10+
11+
if TYPE_CHECKING:
12+
from key_value.aio.stores.windows_registry.store import WindowsRegistryStore
13+
14+
15+
@pytest.mark.skipif(condition=not detect_on_windows(), reason="WindowsRegistryStore is only available on Windows")
16+
class TestWindowsRegistryStore(BaseStoreTests):
17+
@override
18+
@pytest.fixture
19+
async def store(self) -> "WindowsRegistryStore":
20+
# Use a test-specific root to avoid conflicts
21+
from key_value.aio.stores.windows_registry.store import WindowsRegistryStore
22+
23+
store = WindowsRegistryStore(registry_path="software\\py-key-value-test", hive="HKEY_CURRENT_USER")
24+
await store.delete_many(collection="test", keys=["test"])
25+
await store.delete_many(collection="test_collection", keys=["test_key"])
26+
27+
return store
28+
29+
@override
30+
@pytest.mark.skip(reason="We do not test boundedness of registry stores")
31+
async def test_not_unbounded(self, store: BaseStore): ...
32+
33+
@override
34+
@pytest.mark.parametrize(argnames=LARGE_TEST_DATA_ARGNAMES, argvalues=LARGE_TEST_DATA_ARGVALUES, ids=LARGE_TEST_DATA_IDS)
35+
async def test_get_large_put_get(self, store: BaseStore, data: dict[str, Any], json: str):
36+
await store.put(collection="test", key="test", value=data)
37+
assert await store.get(collection="test", key="test") == data
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# WARNING: this file is auto-generated by 'build_sync_library.py'
2+
# from the original file '__init__.py'
3+
# DO NOT CHANGE! Change the original file instead.
4+
from key_value.sync.code_gen.stores.windows_registry.store import WindowsRegistryStore
5+
6+
__all__ = ["WindowsRegistryStore"]
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# WARNING: this file is auto-generated by 'build_sync_library.py'
2+
# from the original file 'store.py'
3+
# DO NOT CHANGE! Change the original file instead.
4+
"""Windows Registry-based key-value store."""
5+
6+
from typing import Literal
7+
from winreg import HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE
8+
9+
from key_value.shared.utils.managed_entry import ManagedEntry
10+
from key_value.shared.utils.sanitize import ALPHANUMERIC_CHARACTERS, sanitize_string
11+
from typing_extensions import override
12+
13+
from key_value.sync.code_gen.stores.base import BaseStore
14+
15+
try:
16+
import winreg
17+
except ImportError as e:
18+
msg = "WindowsRegistryStore requires Windows platform (winreg module)"
19+
raise ImportError(msg) from e
20+
21+
MAX_KEY_LENGTH = 96
22+
ALLOWED_KEY_CHARACTERS: str = ALPHANUMERIC_CHARACTERS
23+
24+
MAX_COLLECTION_LENGTH = 96
25+
ALLOWED_COLLECTION_CHARACTERS: str = ALPHANUMERIC_CHARACTERS
26+
DEFAULT_REGISTRY_PATH = "Software\\py-key-value"
27+
DEFAULT_HIVE = "HKEY_CURRENT_USER"
28+
29+
30+
class WindowsRegistryStore(BaseStore):
31+
"""Windows Registry-based key-value store.
32+
33+
This store uses the Windows Registry to persist key-value pairs. Each entry is stored
34+
as a string value in the registry under HKEY_CURRENT_USER\\Software\\{root}\\{collection}\\{key}.
35+
36+
Note: TTL is not natively supported by Windows Registry, so TTL information is stored
37+
within the JSON payload and checked at retrieval time.
38+
"""
39+
40+
def __init__(
41+
self,
42+
*,
43+
hive: Literal["HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE"] | None = None,
44+
registry_path: str | None = None,
45+
default_collection: str | None = None,
46+
) -> None:
47+
"""Initialize the Windows Registry store.
48+
49+
Args:
50+
hive: The hive to use. Defaults to "HKEY_CURRENT_USER".
51+
registry_path: The registry path to use. Must be a valid registry path under the hive. Defaults to "Software\\py-key-value".
52+
default_collection: The default collection to use if no collection is provided.
53+
"""
54+
self._hive = HKEY_LOCAL_MACHINE if hive == "HKEY_LOCAL_MACHINE" else HKEY_CURRENT_USER
55+
self._registry_path = registry_path or DEFAULT_REGISTRY_PATH
56+
57+
super().__init__(default_collection=default_collection)
58+
59+
def _sanitize_collection_name(self, collection: str) -> str:
60+
return sanitize_string(value=collection, max_length=MAX_COLLECTION_LENGTH, allowed_characters=ALLOWED_COLLECTION_CHARACTERS)
61+
62+
def _sanitize_key(self, key: str) -> str:
63+
return sanitize_string(value=key, max_length=MAX_KEY_LENGTH, allowed_characters=ALLOWED_KEY_CHARACTERS)
64+
65+
def _get_registry_path(self, *, collection: str) -> str:
66+
"""Get the full registry path for a collection."""
67+
sanitized_collection = self._sanitize_collection_name(collection=collection)
68+
return f"{self._registry_path}\\{sanitized_collection}"
69+
70+
@override
71+
def _get_managed_entry(self, *, key: str, collection: str) -> ManagedEntry | None:
72+
sanitized_key = self._sanitize_key(key=key)
73+
registry_path = self._get_registry_path(collection=collection)
74+
75+
try:
76+
with winreg.OpenKey(key=self._hive, sub_key=registry_path, reserved=0, access=winreg.KEY_READ) as reg_key:
77+
(json_str, _) = winreg.QueryValueEx(reg_key, sanitized_key)
78+
except (FileNotFoundError, OSError):
79+
return None
80+
81+
if json_str is None:
82+
return None
83+
84+
return ManagedEntry.from_json(json_str=json_str)
85+
86+
@override
87+
def _put_managed_entry(self, *, key: str, collection: str, managed_entry: ManagedEntry) -> None:
88+
sanitized_key = self._sanitize_key(key=key)
89+
registry_path = self._get_registry_path(collection=collection)
90+
91+
json_str: str = managed_entry.to_json()
92+
93+
try:
94+
with winreg.CreateKey(self._hive, registry_path) as reg_key:
95+
winreg.SetValueEx(reg_key, sanitized_key, 0, winreg.REG_SZ, json_str)
96+
except OSError as e:
97+
msg = f"Failed to write to registry: {e}"
98+
raise RuntimeError(msg) from e
99+
100+
@override
101+
def _delete_managed_entry(self, *, key: str, collection: str) -> bool:
102+
sanitized_key = self._sanitize_key(key=key)
103+
registry_path = self._get_registry_path(collection=collection)
104+
105+
try:
106+
with winreg.OpenKey(self._hive, registry_path, 0, winreg.KEY_WRITE) as reg_key:
107+
winreg.DeleteValue(reg_key, sanitized_key)
108+
except (FileNotFoundError, OSError):
109+
return False
110+
else:
111+
return True
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# WARNING: this file is auto-generated by 'build_sync_library.py'
2+
# from the original file '__init__.py'
3+
# DO NOT CHANGE! Change the original file instead.
4+
from key_value.sync.code_gen.stores.windows_registry.store import WindowsRegistryStore
5+
6+
__all__ = ["WindowsRegistryStore"]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# WARNING: this file is auto-generated by 'build_sync_library.py'
2+
# from the original file '__init__.py'
3+
# DO NOT CHANGE! Change the original file instead.
4+

0 commit comments

Comments
 (0)