├── key-value ├── key-value-shared │ ├── py.typed │ ├── tests │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── test_managed_entry.py │ │ │ └── test_time_to_live.py │ ├── src │ │ └── key_value │ │ │ └── shared │ │ │ ├── utils │ │ │ ├── __init__.py │ │ │ └── time_to_live.py │ │ │ ├── errors │ │ │ ├── wrappers │ │ │ │ ├── __init__.py │ │ │ │ ├── read_only.py │ │ │ │ ├── encryption.py │ │ │ │ └── limit_size.py │ │ │ ├── store.py │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ └── key_value.py │ │ │ ├── constants.py │ │ │ ├── __init__.py │ │ │ ├── code_gen │ │ │ ├── sleep.py │ │ │ ├── gather.py │ │ │ └── run.py │ │ │ ├── stores │ │ │ └── wait.py │ │ │ └── type_checking │ │ │ └── bear_spray.py │ ├── README.md │ └── pyproject.toml ├── key-value-aio │ ├── tests │ │ ├── __init__.py │ │ ├── stores │ │ │ ├── __init__.py │ │ │ ├── disk │ │ │ │ ├── __init__.py │ │ │ │ ├── test_disk.py │ │ │ │ └── test_multi_disk.py │ │ │ ├── keyring │ │ │ │ └── __init__.py │ │ │ ├── memory │ │ │ │ ├── __init__.py │ │ │ │ └── test_memory.py │ │ │ ├── redis │ │ │ │ └── __init__.py │ │ │ ├── rocksdb │ │ │ │ └── __init__.py │ │ │ ├── simple │ │ │ │ ├── __init__.py │ │ │ │ └── test_store.py │ │ │ ├── vault │ │ │ │ └── __init__.py │ │ │ ├── wrappers │ │ │ │ ├── __init__.py │ │ │ │ ├── test_statistics.py │ │ │ │ ├── test_prefix_key.py │ │ │ │ ├── test_single_collection.py │ │ │ │ ├── test_prefix_collection.py │ │ │ │ ├── test_passthrough_cache.py │ │ │ │ ├── test_ttl_clamp.py │ │ │ │ ├── test_timeout.py │ │ │ │ ├── test_read_only.py │ │ │ │ └── test_retry.py │ │ │ ├── elasticsearch │ │ │ │ └── __init__.py │ │ │ ├── windows_registry │ │ │ │ ├── __init__.py │ │ │ │ └── test_windows_registry.py │ │ │ ├── duckdb │ │ │ │ └── __init__.py │ │ │ ├── dynamodb │ │ │ │ └── __init__.py │ │ │ ├── conftest.py │ │ │ └── filetree │ │ │ │ └── test_filetree.py │ │ ├── adapters │ │ │ ├── __init__.py │ │ │ └── test_raise.py │ │ ├── protocols │ │ │ ├── __init__.py │ │ │ └── test_types.py │ │ └── cases.py │ ├── src │ │ └── key_value │ │ │ └── aio │ │ │ ├── py.typed │ │ │ ├── stores │ │ │ ├── __init__.py │ │ │ ├── null │ │ │ │ ├── __init__.py │ │ │ │ └── store.py │ │ │ ├── redis │ │ │ │ └── __init__.py │ │ │ ├── vault │ │ │ │ └── __init__.py │ │ │ ├── duckdb │ │ │ │ └── __init__.py │ │ │ ├── keyring │ │ │ │ └── __init__.py │ │ │ ├── memory │ │ │ │ └── __init__.py │ │ │ ├── mongodb │ │ │ │ └── __init__.py │ │ │ ├── rocksdb │ │ │ │ └── __init__.py │ │ │ ├── simple │ │ │ │ └── __init__.py │ │ │ ├── valkey │ │ │ │ └── __init__.py │ │ │ ├── elasticsearch │ │ │ │ └── __init__.py │ │ │ ├── windows_registry │ │ │ │ ├── __init__.py │ │ │ │ └── utils.py │ │ │ ├── dynamodb │ │ │ │ └── __init__.py │ │ │ ├── disk │ │ │ │ └── __init__.py │ │ │ ├── memcached │ │ │ │ └── __init__.py │ │ │ └── filetree │ │ │ │ └── __init__.py │ │ │ ├── wrappers │ │ │ ├── __init__.py │ │ │ ├── retry │ │ │ │ └── __init__.py │ │ │ ├── logging │ │ │ │ └── __init__.py │ │ │ ├── timeout │ │ │ │ ├── __init__.py │ │ │ │ └── wrapper.py │ │ │ ├── fallback │ │ │ │ └── __init__.py │ │ │ ├── limit_size │ │ │ │ └── __init__.py │ │ │ ├── read_only │ │ │ │ └── __init__.py │ │ │ ├── ttl_clamp │ │ │ │ ├── __init__.py │ │ │ │ └── wrapper.py │ │ │ ├── prefix_keys │ │ │ │ └── __init__.py │ │ │ ├── statistics │ │ │ │ └── __init__.py │ │ │ ├── compression │ │ │ │ └── __init__.py │ │ │ ├── passthrough_cache │ │ │ │ └── __init__.py │ │ │ ├── single_collection │ │ │ │ └── __init__.py │ │ │ ├── prefix_collections │ │ │ │ └── __init__.py │ │ │ ├── default_value │ │ │ │ ├── __init__.py │ │ │ │ └── wrapper.py │ │ │ ├── encryption │ │ │ │ └── __init__.py │ │ │ ├── routing │ │ │ │ ├── __init__.py │ │ │ │ └── collection_routing.py │ │ │ └── base.py │ │ │ ├── adapters │ │ │ ├── __init__.py │ │ │ ├── base │ │ │ │ └── __init__.py │ │ │ ├── pydantic │ │ │ │ ├── __init__.py │ │ │ │ └── adapter.py │ │ │ ├── dataclass │ │ │ │ ├── __init__.py │ │ │ │ └── adapter.py │ │ │ └── raise_on_missing │ │ │ │ └── __init__.py │ │ │ ├── protocols │ │ │ └── __init__.py │ │ │ └── __init__.py │ ├── README.md │ ├── .vscode │ │ └── settings.json │ └── pyproject.toml ├── key-value-sync │ ├── tests │ │ ├── __init__.py │ │ └── code_gen │ │ │ ├── __init__.py │ │ │ ├── adapters │ │ │ ├── __init__.py │ │ │ └── test_raise.py │ │ │ ├── stores │ │ │ ├── __init__.py │ │ │ ├── disk │ │ │ │ ├── __init__.py │ │ │ │ ├── test_disk.py │ │ │ │ └── test_multi_disk.py │ │ │ ├── memory │ │ │ │ ├── __init__.py │ │ │ │ └── test_memory.py │ │ │ ├── redis │ │ │ │ └── __init__.py │ │ │ ├── simple │ │ │ │ ├── __init__.py │ │ │ │ └── test_store.py │ │ │ ├── vault │ │ │ │ └── __init__.py │ │ │ ├── keyring │ │ │ │ └── __init__.py │ │ │ ├── rocksdb │ │ │ │ └── __init__.py │ │ │ ├── wrappers │ │ │ │ ├── __init__.py │ │ │ │ ├── test_statistics.py │ │ │ │ ├── test_prefix_key.py │ │ │ │ ├── test_single_collection.py │ │ │ │ ├── test_prefix_collection.py │ │ │ │ ├── test_passthrough_cache.py │ │ │ │ ├── test_ttl_clamp.py │ │ │ │ └── test_read_only.py │ │ │ ├── elasticsearch │ │ │ │ └── __init__.py │ │ │ ├── windows_registry │ │ │ │ ├── __init__.py │ │ │ │ └── test_windows_registry.py │ │ │ ├── duckdb │ │ │ │ └── __init__.py │ │ │ └── conftest.py │ │ │ ├── protocols │ │ │ ├── __init__.py │ │ │ └── test_types.py │ │ │ └── cases.py │ ├── src │ │ └── key_value │ │ │ └── sync │ │ │ ├── py.typed │ │ │ ├── code_gen │ │ │ ├── __init__.py │ │ │ ├── adapters │ │ │ │ ├── __init__.py │ │ │ │ ├── base │ │ │ │ │ └── __init__.py │ │ │ │ ├── pydantic │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── adapter.py │ │ │ │ ├── dataclass │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── adapter.py │ │ │ │ └── raise_on_missing │ │ │ │ │ └── __init__.py │ │ │ ├── stores │ │ │ │ ├── __init__.py │ │ │ │ ├── null │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── store.py │ │ │ │ ├── duckdb │ │ │ │ │ └── __init__.py │ │ │ │ ├── memory │ │ │ │ │ └── __init__.py │ │ │ │ ├── redis │ │ │ │ │ └── __init__.py │ │ │ │ ├── simple │ │ │ │ │ └── __init__.py │ │ │ │ ├── valkey │ │ │ │ │ └── __init__.py │ │ │ │ ├── vault │ │ │ │ │ └── __init__.py │ │ │ │ ├── keyring │ │ │ │ │ └── __init__.py │ │ │ │ ├── mongodb │ │ │ │ │ └── __init__.py │ │ │ │ ├── rocksdb │ │ │ │ │ └── __init__.py │ │ │ │ ├── elasticsearch │ │ │ │ │ └── __init__.py │ │ │ │ ├── windows_registry │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── utils.py │ │ │ │ └── disk │ │ │ │ │ └── __init__.py │ │ │ ├── wrappers │ │ │ │ ├── __init__.py │ │ │ │ ├── retry │ │ │ │ │ └── __init__.py │ │ │ │ ├── logging │ │ │ │ │ └── __init__.py │ │ │ │ ├── fallback │ │ │ │ │ └── __init__.py │ │ │ │ ├── read_only │ │ │ │ │ └── __init__.py │ │ │ │ ├── ttl_clamp │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── wrapper.py │ │ │ │ ├── limit_size │ │ │ │ │ └── __init__.py │ │ │ │ ├── compression │ │ │ │ │ └── __init__.py │ │ │ │ ├── prefix_keys │ │ │ │ │ └── __init__.py │ │ │ │ ├── statistics │ │ │ │ │ └── __init__.py │ │ │ │ ├── passthrough_cache │ │ │ │ │ └── __init__.py │ │ │ │ ├── single_collection │ │ │ │ │ └── __init__.py │ │ │ │ ├── prefix_collections │ │ │ │ │ └── __init__.py │ │ │ │ ├── default_value │ │ │ │ │ └── __init__.py │ │ │ │ ├── encryption │ │ │ │ │ └── __init__.py │ │ │ │ ├── routing │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── collection_routing.py │ │ │ │ └── base.py │ │ │ └── protocols │ │ │ │ └── __init__.py │ │ │ ├── adapters │ │ │ ├── __init__.py │ │ │ ├── base │ │ │ │ └── __init__.py │ │ │ ├── pydantic │ │ │ │ └── __init__.py │ │ │ ├── dataclass │ │ │ │ └── __init__.py │ │ │ └── raise_on_missing │ │ │ │ └── __init__.py │ │ │ ├── stores │ │ │ ├── __init__.py │ │ │ ├── null │ │ │ │ └── __init__.py │ │ │ ├── redis │ │ │ │ └── __init__.py │ │ │ ├── vault │ │ │ │ └── __init__.py │ │ │ ├── duckdb │ │ │ │ └── __init__.py │ │ │ ├── keyring │ │ │ │ └── __init__.py │ │ │ ├── memory │ │ │ │ └── __init__.py │ │ │ ├── mongodb │ │ │ │ └── __init__.py │ │ │ ├── rocksdb │ │ │ │ └── __init__.py │ │ │ ├── simple │ │ │ │ └── __init__.py │ │ │ ├── valkey │ │ │ │ └── __init__.py │ │ │ ├── elasticsearch │ │ │ │ └── __init__.py │ │ │ ├── windows_registry │ │ │ │ └── __init__.py │ │ │ └── disk │ │ │ │ └── __init__.py │ │ │ ├── wrappers │ │ │ ├── __init__.py │ │ │ ├── retry │ │ │ │ └── __init__.py │ │ │ ├── logging │ │ │ │ └── __init__.py │ │ │ ├── fallback │ │ │ │ └── __init__.py │ │ │ ├── limit_size │ │ │ │ └── __init__.py │ │ │ ├── read_only │ │ │ │ └── __init__.py │ │ │ ├── ttl_clamp │ │ │ │ └── __init__.py │ │ │ ├── prefix_keys │ │ │ │ └── __init__.py │ │ │ ├── statistics │ │ │ │ └── __init__.py │ │ │ ├── compression │ │ │ │ └── __init__.py │ │ │ ├── passthrough_cache │ │ │ │ └── __init__.py │ │ │ ├── single_collection │ │ │ │ └── __init__.py │ │ │ ├── prefix_collections │ │ │ │ └── __init__.py │ │ │ ├── default_value │ │ │ │ └── __init__.py │ │ │ ├── encryption │ │ │ │ └── __init__.py │ │ │ └── routing │ │ │ │ └── __init__.py │ │ │ ├── protocols │ │ │ └── __init__.py │ │ │ └── __init__.py │ ├── README.md │ ├── .vscode │ │ └── settings.json │ └── pyproject.toml └── key-value-shared-test │ ├── src │ └── key_value │ │ └── shared_test │ │ ├── py.typed │ │ └── __init__.py │ ├── README.md │ └── pyproject.toml ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── sonar-project.properties ├── .markdownlint.jsonc ├── docs ├── api │ ├── protocols.md │ ├── adapters.md │ ├── index.md │ ├── stores.md │ └── wrappers.md └── index.md ├── .github └── workflows │ ├── docs.yml │ └── publish.yml ├── .devcontainer └── devcontainer.json ├── mkdocs.yml ├── pyproject.toml └── DEVELOPING.md /key-value/key-value-shared/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/disk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-shared/tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/keyring/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/memory/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/redis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/rocksdb/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/simple/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/vault/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/elasticsearch/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/windows_registry/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-shared-test/src/key_value/shared_test/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-shared-test/src/key_value/shared_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/errors/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/duckdb/__init__.py: -------------------------------------------------------------------------------- 1 | # DuckDB store tests 2 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/dynamodb/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for DynamoDB store.""" 2 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT_COLLECTION_NAME = "default_collection" 2 | -------------------------------------------------------------------------------- /key-value/key-value-shared/README.md: -------------------------------------------------------------------------------- 1 | # py-key-value-shared 2 | 3 | Shared code between key-value-aio and key-value-sync 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/protocols/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.protocols.key_value import AsyncKeyValue as AsyncKeyValue 2 | -------------------------------------------------------------------------------- /key-value/key-value-shared-test/README.md: -------------------------------------------------------------------------------- 1 | # py-key-value-shared-test 2 | 3 | Shared data for tests between key-value-aio and key-value-sync 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/null/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.null.store import NullStore 2 | 3 | __all__ = ["NullStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/redis/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.redis.store import RedisStore 2 | 3 | __all__ = ["RedisStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/vault/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.vault.store import VaultStore 2 | 3 | __all__ = ["VaultStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/duckdb/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.duckdb.store import DuckDBStore 2 | 3 | __all__ = ["DuckDBStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/keyring/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.keyring.store import KeyringStore 2 | 3 | __all__ = ["KeyringStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/memory/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.memory.store import MemoryStore 2 | 3 | __all__ = ["MemoryStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/mongodb/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.mongodb.store import MongoDBStore 2 | 3 | __all__ = ["MongoDBStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/rocksdb/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.rocksdb.store import RocksDBStore 2 | 3 | __all__ = ["RocksDBStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/simple/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.simple.store import SimpleStore 2 | 3 | __all__ = ["SimpleStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/valkey/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.valkey.store import ValkeyStore 2 | 3 | __all__ = ["ValkeyStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/retry/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.retry.wrapper import RetryWrapper 2 | 3 | __all__ = ["RetryWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/logging/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.logging.wrapper import LoggingWrapper 2 | 3 | __all__ = ["LoggingWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/timeout/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.timeout.wrapper import TimeoutWrapper 2 | 3 | __all__ = ["TimeoutWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/README.md: -------------------------------------------------------------------------------- 1 | # py-key-value-aio 2 | 3 | See the [README.md for the project on GitHub](https://github.com/strawgate/py-key-value) 4 | for more information. 5 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/adapters/base/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.adapters.pydantic.base import BasePydanticAdapter 2 | 3 | __all__ = ["BasePydanticAdapter"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/adapters/pydantic/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.adapters.pydantic.adapter import PydanticAdapter 2 | 3 | __all__ = ["PydanticAdapter"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/fallback/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.fallback.wrapper import FallbackWrapper 2 | 3 | __all__ = ["FallbackWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-sync/README.md: -------------------------------------------------------------------------------- 1 | # py-key-value-sync 2 | 3 | See the [README.md for the project on GitHub](https://github.com/strawgate/py-key-value) 4 | for more information. 5 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/adapters/dataclass/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.adapters.dataclass.adapter import DataclassAdapter 2 | 3 | __all__ = ["DataclassAdapter"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/limit_size/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.limit_size.wrapper import LimitSizeWrapper 2 | 3 | __all__ = ["LimitSizeWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/read_only/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.read_only.wrapper import ReadOnlyWrapper 2 | 3 | __all__ = ["ReadOnlyWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/ttl_clamp/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.ttl_clamp.wrapper import TTLClampWrapper 2 | 3 | __all__ = ["TTLClampWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.elasticsearch.store import ElasticsearchStore 2 | 3 | __all__ = ["ElasticsearchStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/prefix_keys/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.prefix_keys.wrapper import PrefixKeysWrapper 2 | 3 | __all__ = ["PrefixKeysWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/statistics/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.statistics.wrapper import StatisticsWrapper 2 | 3 | __all__ = ["StatisticsWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/compression/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.compression.wrapper import CompressionWrapper 2 | 3 | __all__ = ["CompressionWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/windows_registry/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.windows_registry.store import WindowsRegistryStore 2 | 3 | __all__ = ["WindowsRegistryStore"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/adapters/raise_on_missing/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.adapters.raise_on_missing.adapter import RaiseOnMissingAdapter 2 | 3 | __all__ = ["RaiseOnMissingAdapter"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/dynamodb/__init__.py: -------------------------------------------------------------------------------- 1 | """DynamoDB key-value store.""" 2 | 3 | from key_value.aio.stores.dynamodb.store import DynamoDBStore 4 | 5 | __all__ = ["DynamoDBStore"] 6 | -------------------------------------------------------------------------------- /key-value/key-value-aio/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | } -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/passthrough_cache/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.passthrough_cache.wrapper import PassthroughCacheWrapper 2 | 3 | __all__ = ["PassthroughCacheWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/single_collection/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.single_collection.wrapper import SingleCollectionWrapper 2 | 3 | __all__ = ["SingleCollectionWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-sync/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | } -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/prefix_collections/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.prefix_collections.wrapper import PrefixCollectionsWrapper 2 | 3 | __all__ = ["PrefixCollectionsWrapper"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/adapters/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/adapters/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/protocols/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/disk/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/memory/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/redis/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/simple/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/vault/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/disk/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.disk.multi_store import MultiDiskStore 2 | from key_value.aio.stores.disk.store import DiskStore 3 | 4 | __all__ = ["DiskStore", "MultiDiskStore"] 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/keyring/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/rocksdb/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/wrappers/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/adapters/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/elasticsearch/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/windows_registry/__init__.py: -------------------------------------------------------------------------------- 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 | 5 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/memcached/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.stores.memcached.store import MemcachedStore, MemcachedV1KeySanitizationStrategy 2 | 3 | __all__ = ["MemcachedStore", "MemcachedV1KeySanitizationStrategy"] 4 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/duckdb/__init__.py: -------------------------------------------------------------------------------- 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 | # DuckDB store tests 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.unittestEnabled": false, 3 | "python.testing.pytestEnabled": true, 4 | "python.testing.pytestArgs": [ 5 | "key-value", 6 | "--import-mode=importlib", 7 | "-vv", 8 | ], 9 | 10 | } -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/default_value/__init__.py: -------------------------------------------------------------------------------- 1 | """Default value wrapper for returning fallback values when keys are not found.""" 2 | 3 | from key_value.aio.wrappers.default_value.wrapper import DefaultValueWrapper 4 | 5 | __all__ = ["DefaultValueWrapper"] 6 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/encryption/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.encryption.base import BaseEncryptionWrapper 2 | from key_value.aio.wrappers.encryption.fernet import FernetEncryptionWrapper 3 | 4 | __all__ = ["BaseEncryptionWrapper", "FernetEncryptionWrapper"] 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/protocols/__init__.py: -------------------------------------------------------------------------------- 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.protocols.key_value import KeyValue as KeyValue 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/protocols/__init__.py: -------------------------------------------------------------------------------- 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.protocols.key_value import KeyValue as KeyValue 5 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/routing/__init__.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.wrappers.routing.collection_routing import CollectionRoutingWrapper 2 | from key_value.aio.wrappers.routing.wrapper import RoutingFunction, RoutingWrapper 3 | 4 | __all__ = ["CollectionRoutingWrapper", "RoutingFunction", "RoutingWrapper"] 5 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/null/__init__.py: -------------------------------------------------------------------------------- 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.null.store import NullStore 5 | 6 | __all__ = ["NullStore"] 7 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # SonarQube project configuration 2 | sonar.projectKey=strawgate_py-key-value 3 | sonar.organization=strawgate 4 | 5 | # Exclude code-generated sync library from duplication detection 6 | # The sync library is automatically generated from the async library 7 | sonar.cpd.exclusions=**/key-value-sync/**/*.py 8 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/redis/__init__.py: -------------------------------------------------------------------------------- 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.redis.store import RedisStore 5 | 6 | __all__ = ["RedisStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/vault/__init__.py: -------------------------------------------------------------------------------- 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.vault.store import VaultStore 5 | 6 | __all__ = ["VaultStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/null/__init__.py: -------------------------------------------------------------------------------- 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.null.store import NullStore 5 | 6 | __all__ = ["NullStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/duckdb/__init__.py: -------------------------------------------------------------------------------- 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.duckdb.store import DuckDBStore 5 | 6 | __all__ = ["DuckDBStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/keyring/__init__.py: -------------------------------------------------------------------------------- 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.keyring.store import KeyringStore 5 | 6 | __all__ = ["KeyringStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/memory/__init__.py: -------------------------------------------------------------------------------- 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.memory.store import MemoryStore 5 | 6 | __all__ = ["MemoryStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/mongodb/__init__.py: -------------------------------------------------------------------------------- 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.mongodb.store import MongoDBStore 5 | 6 | __all__ = ["MongoDBStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/rocksdb/__init__.py: -------------------------------------------------------------------------------- 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.rocksdb.store import RocksDBStore 5 | 6 | __all__ = ["RocksDBStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/simple/__init__.py: -------------------------------------------------------------------------------- 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.simple.store import SimpleStore 5 | 6 | __all__ = ["SimpleStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/valkey/__init__.py: -------------------------------------------------------------------------------- 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.valkey.store import ValkeyStore 5 | 6 | __all__ = ["ValkeyStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/duckdb/__init__.py: -------------------------------------------------------------------------------- 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.duckdb.store import DuckDBStore 5 | 6 | __all__ = ["DuckDBStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/memory/__init__.py: -------------------------------------------------------------------------------- 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.memory.store import MemoryStore 5 | 6 | __all__ = ["MemoryStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/redis/__init__.py: -------------------------------------------------------------------------------- 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.redis.store import RedisStore 5 | 6 | __all__ = ["RedisStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/simple/__init__.py: -------------------------------------------------------------------------------- 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.simple.store import SimpleStore 5 | 6 | __all__ = ["SimpleStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/valkey/__init__.py: -------------------------------------------------------------------------------- 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.valkey.store import ValkeyStore 5 | 6 | __all__ = ["ValkeyStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/vault/__init__.py: -------------------------------------------------------------------------------- 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.vault.store import VaultStore 5 | 6 | __all__ = ["VaultStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/retry/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.retry.wrapper import RetryWrapper 5 | 6 | __all__ = ["RetryWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/keyring/__init__.py: -------------------------------------------------------------------------------- 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.keyring.store import KeyringStore 5 | 6 | __all__ = ["KeyringStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/mongodb/__init__.py: -------------------------------------------------------------------------------- 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.mongodb.store import MongoDBStore 5 | 6 | __all__ = ["MongoDBStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/rocksdb/__init__.py: -------------------------------------------------------------------------------- 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.rocksdb.store import RocksDBStore 5 | 6 | __all__ = ["RocksDBStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/logging/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.logging.wrapper import LoggingWrapper 5 | 6 | __all__ = ["LoggingWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/adapters/base/__init__.py: -------------------------------------------------------------------------------- 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.adapters.pydantic.base import BasePydanticAdapter 5 | 6 | __all__ = ["BasePydanticAdapter"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/adapters/pydantic/__init__.py: -------------------------------------------------------------------------------- 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.adapters.pydantic.adapter import PydanticAdapter 5 | 6 | __all__ = ["PydanticAdapter"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/retry/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.retry.wrapper import RetryWrapper 5 | 6 | __all__ = ["RetryWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/fallback/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.fallback.wrapper import FallbackWrapper 5 | 6 | __all__ = ["FallbackWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/adapters/dataclass/__init__.py: -------------------------------------------------------------------------------- 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.adapters.dataclass.adapter import DataclassAdapter 5 | 6 | __all__ = ["DataclassAdapter"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/logging/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.logging.wrapper import LoggingWrapper 5 | 6 | __all__ = ["LoggingWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/limit_size/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.limit_size.wrapper import LimitSizeWrapper 5 | 6 | __all__ = ["LimitSizeWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/read_only/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.read_only.wrapper import ReadOnlyWrapper 5 | 6 | __all__ = ["ReadOnlyWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/ttl_clamp/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.ttl_clamp.wrapper import TTLClampWrapper 5 | 6 | __all__ = ["TTLClampWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/adapters/base/__init__.py: -------------------------------------------------------------------------------- 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.adapters.pydantic.base import BasePydanticAdapter 5 | 6 | __all__ = ["BasePydanticAdapter"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/__init__.py: -------------------------------------------------------------------------------- 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.adapters.pydantic.adapter import PydanticAdapter 5 | 6 | __all__ = ["PydanticAdapter"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/fallback/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.fallback.wrapper import FallbackWrapper 5 | 6 | __all__ = ["FallbackWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/read_only/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.read_only.wrapper import ReadOnlyWrapper 5 | 6 | __all__ = ["ReadOnlyWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/ttl_clamp/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.ttl_clamp.wrapper import TTLClampWrapper 5 | 6 | __all__ = ["TTLClampWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/elasticsearch/__init__.py: -------------------------------------------------------------------------------- 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.elasticsearch.store import ElasticsearchStore 5 | 6 | __all__ = ["ElasticsearchStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/prefix_keys/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.prefix_keys.wrapper import PrefixKeysWrapper 5 | 6 | __all__ = ["PrefixKeysWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/statistics/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.statistics.wrapper import StatisticsWrapper 5 | 6 | __all__ = ["StatisticsWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/adapters/dataclass/__init__.py: -------------------------------------------------------------------------------- 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.adapters.dataclass.adapter import DataclassAdapter 5 | 6 | __all__ = ["DataclassAdapter"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/limit_size/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.limit_size.wrapper import LimitSizeWrapper 5 | 6 | __all__ = ["LimitSizeWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/compression/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.compression.wrapper import CompressionWrapper 5 | 6 | __all__ = ["CompressionWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/__init__.py: -------------------------------------------------------------------------------- 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.elasticsearch.store import ElasticsearchStore 5 | 6 | __all__ = ["ElasticsearchStore"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/compression/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.compression.wrapper import CompressionWrapper 5 | 6 | __all__ = ["CompressionWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/prefix_keys/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.prefix_keys.wrapper import PrefixKeysWrapper 5 | 6 | __all__ = ["PrefixKeysWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/statistics/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.statistics.wrapper import StatisticsWrapper 5 | 6 | __all__ = ["StatisticsWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/windows_registry/__init__.py: -------------------------------------------------------------------------------- 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"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/adapters/raise_on_missing/__init__.py: -------------------------------------------------------------------------------- 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.adapters.raise_on_missing.adapter import RaiseOnMissingAdapter 5 | 6 | __all__ = ["RaiseOnMissingAdapter"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/windows_registry/__init__.py: -------------------------------------------------------------------------------- 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"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/passthrough_cache/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.passthrough_cache.wrapper import PassthroughCacheWrapper 5 | 6 | __all__ = ["PassthroughCacheWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/single_collection/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.single_collection.wrapper import SingleCollectionWrapper 5 | 6 | __all__ = ["SingleCollectionWrapper"] 7 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | // Configuration for markdownlint-cli2 2 | { 3 | "default": true, 4 | "MD013": { 5 | "line_length": 80, 6 | "code_blocks": false, // Don't restrict line length in code blocks 7 | "tables": false // Don't restrict line length in tables 8 | }, 9 | "MD024": { 10 | "siblings_only": true // Allow duplicate headings in different sections 11 | } 12 | } -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/adapters/raise_on_missing/__init__.py: -------------------------------------------------------------------------------- 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.adapters.raise_on_missing.adapter import RaiseOnMissingAdapter 5 | 6 | __all__ = ["RaiseOnMissingAdapter"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/prefix_collections/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.prefix_collections.wrapper import PrefixCollectionsWrapper 5 | 6 | __all__ = ["PrefixCollectionsWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/passthrough_cache/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.passthrough_cache.wrapper import PassthroughCacheWrapper 5 | 6 | __all__ = ["PassthroughCacheWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/single_collection/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.single_collection.wrapper import SingleCollectionWrapper 5 | 6 | __all__ = ["SingleCollectionWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/prefix_collections/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.prefix_collections.wrapper import PrefixCollectionsWrapper 5 | 6 | __all__ = ["PrefixCollectionsWrapper"] 7 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/stores/disk/__init__.py: -------------------------------------------------------------------------------- 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.disk.multi_store import MultiDiskStore 5 | from key_value.sync.code_gen.stores.disk.store import DiskStore 6 | 7 | __all__ = ["DiskStore", "MultiDiskStore"] 8 | -------------------------------------------------------------------------------- /docs/api/protocols.md: -------------------------------------------------------------------------------- 1 | # Protocols 2 | 3 | The `AsyncKeyValue` protocol defines the interface that all stores and wrappers 4 | must implement. This protocol-based design allows for maximum flexibility and 5 | composability. 6 | 7 | ## AsyncKeyValue Protocol 8 | 9 | ::: key_value.aio.protocols.key_value.AsyncKeyValue 10 | options: 11 | show_source: true 12 | members: true 13 | show_root_heading: true 14 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/disk/__init__.py: -------------------------------------------------------------------------------- 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.disk.multi_store import MultiDiskStore 5 | from key_value.sync.code_gen.stores.disk.store import DiskStore 6 | 7 | __all__ = ["DiskStore", "MultiDiskStore"] 8 | -------------------------------------------------------------------------------- /docs/api/adapters.md: -------------------------------------------------------------------------------- 1 | # Adapters API Reference 2 | 3 | Complete API reference for all available adapters. 4 | 5 | ## PydanticAdapter 6 | 7 | ::: key_value.aio.adapters.pydantic.PydanticAdapter 8 | options: 9 | show_source: false 10 | members: true 11 | 12 | ## RaiseOnMissingAdapter 13 | 14 | ::: key_value.aio.adapters.raise_on_missing.RaiseOnMissingAdapter 15 | options: 16 | show_source: false 17 | members: true 18 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/default_value/__init__.py: -------------------------------------------------------------------------------- 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 | """Default value wrapper for returning fallback values when keys are not found.""" 5 | 6 | from key_value.sync.code_gen.wrappers.default_value.wrapper import DefaultValueWrapper 7 | 8 | __all__ = ["DefaultValueWrapper"] 9 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/filetree/__init__.py: -------------------------------------------------------------------------------- 1 | """File-tree based store for visual inspection and testing.""" 2 | 3 | from key_value.aio.stores.filetree.store import ( 4 | FileTreeStore, 5 | FileTreeV1CollectionSanitizationStrategy, 6 | FileTreeV1KeySanitizationStrategy, 7 | ) 8 | 9 | __all__ = [ 10 | "FileTreeStore", 11 | "FileTreeV1CollectionSanitizationStrategy", 12 | "FileTreeV1KeySanitizationStrategy", 13 | ] 14 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/default_value/__init__.py: -------------------------------------------------------------------------------- 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 | """Default value wrapper for returning fallback values when keys are not found.""" 5 | 6 | from key_value.sync.code_gen.wrappers.default_value.wrapper import DefaultValueWrapper 7 | 8 | __all__ = ["DefaultValueWrapper"] 9 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/encryption/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.encryption.base import BaseEncryptionWrapper 5 | from key_value.sync.code_gen.wrappers.encryption.fernet import FernetEncryptionWrapper 6 | 7 | __all__ = ["BaseEncryptionWrapper", "FernetEncryptionWrapper"] 8 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/encryption/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.encryption.base import BaseEncryptionWrapper 5 | from key_value.sync.code_gen.wrappers.encryption.fernet import FernetEncryptionWrapper 6 | 7 | __all__ = ["BaseEncryptionWrapper", "FernetEncryptionWrapper"] 8 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from beartype import ( 4 | BeartypeConf, 5 | BeartypeStrategy, 6 | ) 7 | from beartype.claw import beartype_this_package 8 | 9 | disable_beartype = os.environ.get("PY_KEY_VALUE_DISABLE_BEARTYPE", "false").lower() in ("true", "1", "yes") 10 | 11 | strategy = BeartypeStrategy.O0 if disable_beartype else BeartypeStrategy.O1 12 | 13 | beartype_this_package(conf=BeartypeConf(violation_type=UserWarning, strategy=strategy)) 14 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/errors/store.py: -------------------------------------------------------------------------------- 1 | from key_value.shared.errors.base import BaseKeyValueError 2 | 3 | 4 | class KeyValueStoreError(BaseKeyValueError): 5 | """Base exception for all Key-Value store errors.""" 6 | 7 | 8 | class StoreSetupError(KeyValueStoreError): 9 | """Raised when a store setup fails.""" 10 | 11 | 12 | class StoreConnectionError(KeyValueStoreError): 13 | """Raised when unable to connect to or communicate with the underlying store.""" 14 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from beartype import ( 4 | BeartypeConf, 5 | BeartypeStrategy, 6 | ) 7 | from beartype.claw import beartype_this_package 8 | 9 | disable_beartype = os.environ.get("PY_KEY_VALUE_DISABLE_BEARTYPE", "false").lower() in ("true", "1", "yes") 10 | 11 | strategy = BeartypeStrategy.O0 if disable_beartype else BeartypeStrategy.O1 12 | 13 | beartype_this_package(conf=BeartypeConf(violation_type=UserWarning, strategy=strategy)) 14 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/wrappers/routing/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.routing.collection_routing import CollectionRoutingWrapper 5 | from key_value.sync.code_gen.wrappers.routing.wrapper import RoutingFunction, RoutingWrapper 6 | 7 | __all__ = ["CollectionRoutingWrapper", "RoutingFunction", "RoutingWrapper"] 8 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from beartype import ( 4 | BeartypeConf, 5 | BeartypeStrategy, 6 | ) 7 | from beartype.claw import beartype_this_package 8 | 9 | disable_beartype = os.environ.get("PY_KEY_VALUE_DISABLE_BEARTYPE", "false").lower() in ("true", "1", "yes") 10 | 11 | strategy = BeartypeStrategy.O0 if disable_beartype else BeartypeStrategy.O1 12 | 13 | beartype_this_package(conf=BeartypeConf(violation_type=UserWarning, strategy=strategy)) 14 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/routing/__init__.py: -------------------------------------------------------------------------------- 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.wrappers.routing.collection_routing import CollectionRoutingWrapper 5 | from key_value.sync.code_gen.wrappers.routing.wrapper import RoutingFunction, RoutingWrapper 6 | 7 | __all__ = ["CollectionRoutingWrapper", "RoutingFunction", "RoutingWrapper"] 8 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_statistics.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing_extensions import override 3 | 4 | from key_value.aio.stores.memory.store import MemoryStore 5 | from key_value.aio.wrappers.statistics import StatisticsWrapper 6 | from tests.stores.base import BaseStoreTests 7 | 8 | 9 | class TestStatisticsWrapper(BaseStoreTests): 10 | @override 11 | @pytest.fixture 12 | async def store(self, memory_store: MemoryStore) -> StatisticsWrapper: 13 | return StatisticsWrapper(key_value=memory_store) 14 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/code_gen/sleep.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from typing import SupportsFloat 4 | 5 | 6 | async def asleep(seconds: SupportsFloat) -> None: 7 | """ 8 | Equivalent to asyncio.sleep(), converted to time.sleep() by async_to_sync. 9 | """ 10 | await asyncio.sleep(float(seconds)) 11 | 12 | 13 | def sleep(seconds: SupportsFloat) -> None: 14 | """ 15 | Equivalent to time.sleep(), converted to asyncio.sleep() by async_to_sync. 16 | """ 17 | time.sleep(float(seconds)) 18 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/simple/test_store.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing_extensions import override 3 | 4 | from key_value.aio.stores.simple.store import SimpleStore 5 | from tests.stores.base import BaseStoreTests 6 | 7 | 8 | @pytest.mark.filterwarnings("ignore:A configured store is unstable and may change in a backwards incompatible way. Use at your own risk.") 9 | class TestSimpleStore(BaseStoreTests): 10 | @override 11 | @pytest.fixture 12 | async def store(self) -> SimpleStore: 13 | return SimpleStore(max_entries=500) 14 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_prefix_key.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing_extensions import override 3 | 4 | from key_value.aio.stores.memory.store import MemoryStore 5 | from key_value.aio.wrappers.prefix_keys import PrefixKeysWrapper 6 | from tests.stores.base import BaseStoreTests 7 | 8 | 9 | class TestPrefixKeyWrapper(BaseStoreTests): 10 | @override 11 | @pytest.fixture 12 | async def store(self, memory_store: MemoryStore) -> PrefixKeysWrapper: 13 | return PrefixKeysWrapper(key_value=memory_store, prefix="key_prefix") 14 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_single_collection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing_extensions import override 3 | 4 | from key_value.aio.stores.memory.store import MemoryStore 5 | from key_value.aio.wrappers.single_collection import SingleCollectionWrapper 6 | from tests.stores.base import BaseStoreTests 7 | 8 | 9 | class TestSingleCollectionWrapper(BaseStoreTests): 10 | @override 11 | @pytest.fixture 12 | async def store(self, memory_store: MemoryStore) -> SingleCollectionWrapper: 13 | return SingleCollectionWrapper(key_value=memory_store, single_collection="test") 14 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_prefix_collection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing_extensions import override 3 | 4 | from key_value.aio.stores.memory.store import MemoryStore 5 | from key_value.aio.wrappers.prefix_collections import PrefixCollectionsWrapper 6 | from tests.stores.base import BaseStoreTests 7 | 8 | 9 | class TestPrefixCollectionWrapper(BaseStoreTests): 10 | @override 11 | @pytest.fixture 12 | async def store(self, memory_store: MemoryStore) -> PrefixCollectionsWrapper: 13 | return PrefixCollectionsWrapper(key_value=memory_store, prefix="collection_prefix") 14 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/errors/wrappers/read_only.py: -------------------------------------------------------------------------------- 1 | from key_value.shared.errors.key_value import KeyValueOperationError 2 | 3 | 4 | class ReadOnlyError(KeyValueOperationError): 5 | """Raised when a write operation is attempted on a read-only store.""" 6 | 7 | def __init__(self, operation: str, collection: str | None = None, key: str | None = None): 8 | super().__init__( 9 | message="Write operation not allowed on read-only store.", 10 | extra_info={"operation": operation, "collection": collection or "default", "key": key or "N/A"}, 11 | ) 12 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | 3 | import pytest 4 | 5 | from key_value.aio.stores.memory.store import MemoryStore 6 | 7 | 8 | @pytest.fixture 9 | def memory_store() -> MemoryStore: 10 | return MemoryStore(max_entries_per_collection=500) 11 | 12 | 13 | def now() -> datetime: 14 | return datetime.now(tz=timezone.utc) 15 | 16 | 17 | def now_plus(seconds: int) -> datetime: 18 | return now() + timedelta(seconds=seconds) 19 | 20 | 21 | def is_around(value: float, delta: float = 1) -> bool: 22 | return value - delta < value < value + delta 23 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from .key_value import ( 2 | DeserializationError, 3 | InvalidKeyError, 4 | InvalidTTLError, 5 | KeyValueOperationError, 6 | MissingKeyError, 7 | SerializationError, 8 | ) 9 | from .store import KeyValueStoreError, StoreConnectionError, StoreSetupError 10 | 11 | __all__ = [ 12 | "DeserializationError", 13 | "InvalidKeyError", 14 | "InvalidTTLError", 15 | "KeyValueOperationError", 16 | "KeyValueStoreError", 17 | "MissingKeyError", 18 | "SerializationError", 19 | "StoreConnectionError", 20 | "StoreSetupError", 21 | ] 22 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/errors/wrappers/encryption.py: -------------------------------------------------------------------------------- 1 | from key_value.shared.errors import KeyValueOperationError 2 | 3 | 4 | class EncryptionError(KeyValueOperationError): 5 | """Exception raised when encryption or decryption fails.""" 6 | 7 | 8 | class DecryptionError(EncryptionError): 9 | """Exception raised when decryption fails.""" 10 | 11 | 12 | class EncryptionVersionError(EncryptionError): 13 | """Exception raised when the encryption version is not supported.""" 14 | 15 | 16 | class CorruptedDataError(DecryptionError): 17 | """Exception raised when the encrypted data is corrupted.""" 18 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/code_gen/gather.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections.abc import Awaitable 3 | from typing import Any 4 | 5 | 6 | async def async_gather(*aws: Awaitable[Any], return_exceptions: bool = False) -> list[Any]: 7 | """ 8 | Equivalent to asyncio.gather(), converted to asyncio.gather() by async_to_sync. 9 | """ 10 | return await asyncio.gather(*aws, return_exceptions=return_exceptions) 11 | 12 | 13 | def gather(*args: Any, **kwargs: Any) -> tuple[Any, ...]: 14 | """ 15 | Equivalent to asyncio.gather(), converted to asyncio.gather() by async_to_sync. 16 | """ 17 | return args 18 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/memory/test_memory.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing_extensions import override 3 | 4 | from key_value.aio.stores.memory.store import MemoryStore 5 | from tests.stores.base import BaseStoreTests 6 | 7 | 8 | class TestMemoryStore(BaseStoreTests): 9 | @override 10 | @pytest.fixture 11 | async def store(self) -> MemoryStore: 12 | return MemoryStore(max_entries_per_collection=500) 13 | 14 | async def test_seed(self): 15 | store = MemoryStore(max_entries_per_collection=500, seed={"test_collection": {"test_key": {"obj_key": "obj_value"}}}) 16 | assert await store.get(key="test_key", collection="test_collection") == {"obj_key": "obj_value"} 17 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/simple/test_store.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_store.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from typing_extensions import override 6 | 7 | from key_value.sync.code_gen.stores.simple.store import SimpleStore 8 | from tests.code_gen.stores.base import BaseStoreTests 9 | 10 | 11 | @pytest.mark.filterwarnings("ignore:A configured store is unstable and may change in a backwards incompatible way. Use at your own risk.") 12 | class TestSimpleStore(BaseStoreTests): 13 | @override 14 | @pytest.fixture 15 | def store(self) -> SimpleStore: 16 | return SimpleStore(max_entries=500) 17 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/wrappers/test_statistics.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_statistics.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from typing_extensions import override 6 | 7 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 8 | from key_value.sync.code_gen.wrappers.statistics import StatisticsWrapper 9 | from tests.code_gen.stores.base import BaseStoreTests 10 | 11 | 12 | class TestStatisticsWrapper(BaseStoreTests): 13 | @override 14 | @pytest.fixture 15 | def store(self, memory_store: MemoryStore) -> StatisticsWrapper: 16 | return StatisticsWrapper(key_value=memory_store) 17 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/wrappers/test_prefix_key.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_prefix_key.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from typing_extensions import override 6 | 7 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 8 | from key_value.sync.code_gen.wrappers.prefix_keys import PrefixKeysWrapper 9 | from tests.code_gen.stores.base import BaseStoreTests 10 | 11 | 12 | class TestPrefixKeyWrapper(BaseStoreTests): 13 | @override 14 | @pytest.fixture 15 | def store(self, memory_store: MemoryStore) -> PrefixKeysWrapper: 16 | return PrefixKeysWrapper(key_value=memory_store, prefix="key_prefix") 17 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/conftest.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'conftest.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from datetime import datetime, timedelta, timezone 5 | 6 | import pytest 7 | 8 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 9 | 10 | 11 | @pytest.fixture 12 | def memory_store() -> MemoryStore: 13 | return MemoryStore(max_entries_per_collection=500) 14 | 15 | 16 | def now() -> datetime: 17 | return datetime.now(tz=timezone.utc) 18 | 19 | 20 | def now_plus(seconds: int) -> datetime: 21 | return now() + timedelta(seconds=seconds) 22 | 23 | 24 | def is_around(value: float, delta: float = 1) -> bool: 25 | return value - delta < value < value + delta 26 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/wrappers/test_single_collection.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_single_collection.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from typing_extensions import override 6 | 7 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 8 | from key_value.sync.code_gen.wrappers.single_collection import SingleCollectionWrapper 9 | from tests.code_gen.stores.base import BaseStoreTests 10 | 11 | 12 | class TestSingleCollectionWrapper(BaseStoreTests): 13 | @override 14 | @pytest.fixture 15 | def store(self, memory_store: MemoryStore) -> SingleCollectionWrapper: 16 | return SingleCollectionWrapper(key_value=memory_store, single_collection="test") 17 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/wrappers/test_prefix_collection.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_prefix_collection.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from typing_extensions import override 6 | 7 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 8 | from key_value.sync.code_gen.wrappers.prefix_collections import PrefixCollectionsWrapper 9 | from tests.code_gen.stores.base import BaseStoreTests 10 | 11 | 12 | class TestPrefixCollectionWrapper(BaseStoreTests): 13 | @override 14 | @pytest.fixture 15 | def store(self, memory_store: MemoryStore) -> PrefixCollectionsWrapper: 16 | return PrefixCollectionsWrapper(key_value=memory_store, prefix="collection_prefix") 17 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/null/store.py: -------------------------------------------------------------------------------- 1 | from key_value.shared.utils.managed_entry import ManagedEntry 2 | from typing_extensions import override 3 | 4 | from key_value.aio.stores.base import BaseStore 5 | 6 | 7 | class NullStore(BaseStore): 8 | """Null object pattern store that accepts all operations but stores nothing.""" 9 | 10 | @override 11 | async def _get_managed_entry(self, *, key: str, collection: str) -> ManagedEntry | None: 12 | return None 13 | 14 | @override 15 | async def _put_managed_entry( 16 | self, 17 | *, 18 | key: str, 19 | collection: str, 20 | managed_entry: ManagedEntry, 21 | ) -> None: 22 | pass 23 | 24 | @override 25 | async def _delete_managed_entry(self, *, key: str, collection: str) -> bool: 26 | return False 27 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/protocols/test_types.py: -------------------------------------------------------------------------------- 1 | from key_value.aio.protocols.key_value import AsyncKeyValue 2 | from key_value.aio.stores.memory import MemoryStore 3 | 4 | 5 | async def test_key_value_protocol(): 6 | async def test_protocol(key_value: AsyncKeyValue): 7 | assert await key_value.get(collection="test", key="test") is None 8 | await key_value.put(collection="test", key="test", value={"test": "test"}) 9 | assert await key_value.delete(collection="test", key="test") 10 | await key_value.put(collection="test", key="test_2", value={"test": "test"}) 11 | 12 | memory_store = MemoryStore() 13 | 14 | await test_protocol(key_value=memory_store) 15 | 16 | assert await memory_store.get(collection="test", key="test") is None 17 | assert await memory_store.get(collection="test", key="test_2") == {"test": "test"} 18 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: '3.10' 22 | 23 | - name: Install uv 24 | uses: astral-sh/setup-uv@v5 25 | with: 26 | enable-cache: true 27 | 28 | - name: Install all packages and dependencies 29 | run: uv sync --all-packages --group dev 30 | 31 | - name: Build documentation 32 | run: uv run --extra docs mkdocs build 33 | 34 | - name: Deploy to GitHub Pages 35 | run: uv run --extra docs mkdocs gh-deploy --force 36 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/memory/test_memory.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_memory.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from typing_extensions import override 6 | 7 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 8 | from tests.code_gen.stores.base import BaseStoreTests 9 | 10 | 11 | class TestMemoryStore(BaseStoreTests): 12 | @override 13 | @pytest.fixture 14 | def store(self) -> MemoryStore: 15 | return MemoryStore(max_entries_per_collection=500) 16 | 17 | def test_seed(self): 18 | store = MemoryStore(max_entries_per_collection=500, seed={"test_collection": {"test_key": {"obj_key": "obj_value"}}}) 19 | assert store.get(key="test_key", collection="test_collection") == {"obj_key": "obj_value"} 20 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/stores/wait.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Awaitable, Callable 2 | from typing import SupportsFloat 3 | 4 | from key_value.shared.code_gen.sleep import asleep, sleep 5 | 6 | 7 | async def async_wait_for_true(bool_fn: Callable[[], Awaitable[bool]], tries: int = 10, wait_time: SupportsFloat = 1) -> bool: 8 | """ 9 | Wait for a store to be ready. 10 | """ 11 | for _ in range(tries): 12 | if await bool_fn(): 13 | return True 14 | await asleep(seconds=float(wait_time)) 15 | return False 16 | 17 | 18 | def wait_for_true(bool_fn: Callable[[], bool], tries: int = 10, wait_time: SupportsFloat = 1) -> bool: 19 | """ 20 | Wait for a store to be ready. 21 | """ 22 | for _ in range(tries): 23 | if bool_fn(): 24 | return True 25 | sleep(seconds=float(wait_time)) 26 | return False 27 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/type_checking/bear_spray.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | 3 | from beartype import BeartypeConf, BeartypeStrategy, beartype 4 | from typing_extensions import ParamSpec, TypeVar 5 | 6 | no_bear_type_check_conf = BeartypeConf(strategy=BeartypeStrategy.O0) 7 | 8 | no_bear_type = beartype(conf=no_bear_type_check_conf) 9 | 10 | enforce_bear_type_conf = BeartypeConf(strategy=BeartypeStrategy.O1, violation_type=TypeError) 11 | enforce_bear_type = beartype(conf=enforce_bear_type_conf) 12 | 13 | P = ParamSpec(name="P") 14 | R = TypeVar(name="R") 15 | 16 | 17 | def no_bear_type_check(func: Callable[P, R]) -> Callable[P, R]: 18 | return no_bear_type(func) 19 | 20 | 21 | def bear_enforce(func: Callable[P, R]) -> Callable[P, R]: 22 | """Enforce beartype with exceptions instead of warnings.""" 23 | return enforce_bear_type(func) 24 | 25 | 26 | bear_spray = no_bear_type_check 27 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/null/store.py: -------------------------------------------------------------------------------- 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 | from key_value.shared.utils.managed_entry import ManagedEntry 5 | from typing_extensions import override 6 | 7 | from key_value.sync.code_gen.stores.base import BaseStore 8 | 9 | 10 | class NullStore(BaseStore): 11 | """Null object pattern store that accepts all operations but stores nothing.""" 12 | 13 | @override 14 | def _get_managed_entry(self, *, key: str, collection: str) -> ManagedEntry | None: 15 | return None 16 | 17 | @override 18 | def _put_managed_entry(self, *, key: str, collection: str, managed_entry: ManagedEntry) -> None: 19 | pass 20 | 21 | @override 22 | def _delete_managed_entry(self, *, key: str, collection: str) -> bool: 23 | return False 24 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/errors/base.py: -------------------------------------------------------------------------------- 1 | ExtraInfoType = dict[str, str | int | float | bool | None] 2 | 3 | 4 | class BaseKeyValueError(Exception): 5 | """Base exception for all KV Store Adapter errors.""" 6 | 7 | extra_info: ExtraInfoType | None = None 8 | message: str | None = None 9 | 10 | def __init__(self, message: str | None = None, extra_info: ExtraInfoType | None = None): 11 | message_parts: list[str] = [] 12 | 13 | if message: 14 | message_parts.append(message) 15 | 16 | if extra_info: 17 | extra_info_str = ";".join(f"{k}: {v}" for k, v in extra_info.items()) 18 | if message: 19 | extra_info_str = "(" + extra_info_str + ")" 20 | 21 | message_parts.append(extra_info_str) 22 | 23 | self.message = ": ".join(message_parts) 24 | 25 | super().__init__(self.message) 26 | 27 | self.extra_info = extra_info 28 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/protocols/test_types.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_types.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from key_value.sync.code_gen.protocols.key_value import KeyValue 5 | from key_value.sync.code_gen.stores.memory import MemoryStore 6 | 7 | 8 | def test_key_value_protocol(): 9 | def test_protocol(key_value: KeyValue): 10 | assert key_value.get(collection="test", key="test") is None 11 | key_value.put(collection="test", key="test", value={"test": "test"}) 12 | assert key_value.delete(collection="test", key="test") 13 | key_value.put(collection="test", key="test_2", value={"test": "test"}) 14 | 15 | memory_store = MemoryStore() 16 | 17 | test_protocol(key_value=memory_store) 18 | 19 | assert memory_store.get(collection="test", key="test") is None 20 | assert memory_store.get(collection="test", key="test_2") == {"test": "test"} 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // Simple VS Code tasks that call Makefile targets 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "label": "Bump version (make)", 7 | "type": "shell", 8 | "command": "make", 9 | "args": [ 10 | "bump-version", 11 | "VERSION=${input:newVersion}" 12 | ], 13 | "problemMatcher": [] 14 | }, 15 | { 16 | "label": "Bump version (dry-run) (make)", 17 | "type": "shell", 18 | "command": "make", 19 | "args": [ 20 | "bump-version-dry", 21 | "VERSION=${input:newVersion}" 22 | ], 23 | "problemMatcher": [] 24 | }, 25 | { 26 | "label": "Build sync library (make)", 27 | "type": "shell", 28 | "command": "make", 29 | "args": [ 30 | "build-sync" 31 | ], 32 | "problemMatcher": [] 33 | } 34 | ], 35 | "inputs": [ 36 | { 37 | "id": "newVersion", 38 | "type": "promptString", 39 | "description": "Enter new version (e.g. 1.2.3)", 40 | "default": "0.0.0" 41 | } 42 | ] 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/errors/wrappers/limit_size.py: -------------------------------------------------------------------------------- 1 | from key_value.shared.errors.key_value import KeyValueOperationError 2 | 3 | 4 | class EntryTooLargeError(KeyValueOperationError): 5 | """Raised when an entry exceeds the maximum allowed size.""" 6 | 7 | def __init__(self, size: int, max_size: int, collection: str | None = None, key: str | None = None): 8 | super().__init__( 9 | message="Entry size exceeds the maximum allowed size.", 10 | extra_info={"size": size, "max_size": max_size, "collection": collection or "default", "key": key}, 11 | ) 12 | 13 | 14 | class EntryTooSmallError(KeyValueOperationError): 15 | """Raised when an entry is less than the minimum allowed size.""" 16 | 17 | def __init__(self, size: int, min_size: int, collection: str | None = None, key: str | None = None): 18 | super().__init__( 19 | message="Entry size is less than the minimum allowed size.", 20 | extra_info={"size": size, "min_size": min_size, "collection": collection or "default", "key": key}, 21 | ) 22 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "py-key-value", 3 | "image": "ghcr.io/astral-sh/uv:python3.10-bookworm", 4 | "features": { 5 | "ghcr.io/devcontainers/features/node:1": { 6 | "version": "lts" 7 | }, 8 | "ghcr.io/devcontainers/features/github-cli:1": {}, 9 | "ghcr.io/devcontainers/features/git:1": {} 10 | }, 11 | "runArgs": [ 12 | "--network=host" 13 | ], 14 | "mounts": [ 15 | "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" 16 | ], 17 | "customizations": { 18 | "vscode": { 19 | "extensions": [ 20 | "ms-python.python", 21 | "ms-python.vscode-pylance", 22 | "charliermarsh.ruff", 23 | "DavidAnson.vscode-markdownlint" 24 | ], 25 | "settings": { 26 | "python.defaultInterpreterPath": "/usr/local/bin/python", 27 | "python.testing.pytestEnabled": true, 28 | "python.testing.unittestEnabled": false, 29 | "python.testing.pytestArgs": [ 30 | "key-value", 31 | "--import-mode=importlib", 32 | "-vv" 33 | ] 34 | } 35 | } 36 | }, 37 | "postCreateCommand": "make sync", 38 | "remoteUser": "root" 39 | } -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_passthrough_cache.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from collections.abc import AsyncGenerator 3 | 4 | import pytest 5 | from typing_extensions import override 6 | 7 | from key_value.aio.stores.disk.store import DiskStore 8 | from key_value.aio.stores.memory.store import MemoryStore 9 | from key_value.aio.wrappers.passthrough_cache import PassthroughCacheWrapper 10 | from tests.stores.base import BaseStoreTests 11 | 12 | DISK_STORE_SIZE_LIMIT = 100 * 1024 # 100KB 13 | 14 | 15 | class TestPassthroughCacheWrapper(BaseStoreTests): 16 | @pytest.fixture(scope="session") 17 | async def primary_store(self) -> AsyncGenerator[DiskStore, None]: 18 | with tempfile.TemporaryDirectory() as temp_dir: 19 | async with DiskStore(directory=temp_dir, max_size=DISK_STORE_SIZE_LIMIT) as disk_store: 20 | yield disk_store 21 | 22 | @pytest.fixture 23 | async def cache_store(self, memory_store: MemoryStore) -> MemoryStore: 24 | return memory_store 25 | 26 | @override 27 | @pytest.fixture 28 | async def store(self, primary_store: DiskStore, cache_store: MemoryStore) -> PassthroughCacheWrapper: 29 | primary_store._cache.clear() # pyright: ignore[reportPrivateUsage] 30 | return PassthroughCacheWrapper(primary_key_value=primary_store, cache_key_value=cache_store) 31 | -------------------------------------------------------------------------------- /key-value/key-value-shared/tests/utils/test_managed_entry.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from typing import Any 3 | 4 | from key_value.shared_test.cases import SIMPLE_CASES, PositiveCases 5 | 6 | from key_value.shared.utils.managed_entry import dump_to_json, load_from_json 7 | 8 | FIXED_DATETIME = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc) 9 | FIXED_DATETIME_STRING = FIXED_DATETIME.isoformat() 10 | 11 | 12 | @PositiveCases.parametrize(cases=SIMPLE_CASES) 13 | def test_dump_to_json(data: dict[str, Any], json: str, round_trip: dict[str, Any]): 14 | """Test that the dump_to_json function dumps the data to the matching JSON string""" 15 | assert dump_to_json(data) == json 16 | 17 | 18 | @PositiveCases.parametrize(cases=SIMPLE_CASES) 19 | def test_load_from_json(data: dict[str, Any], json: str, round_trip: dict[str, Any]): 20 | """Test that the load_from_json function loads the data (round-trip) from the matching JSON string""" 21 | assert load_from_json(json) == round_trip 22 | 23 | 24 | @PositiveCases.parametrize(cases=SIMPLE_CASES) 25 | def test_roundtrip_json(data: dict[str, Any], json: str, round_trip: dict[str, Any]): 26 | """Test that the dump_to_json and load_from_json functions roundtrip the data""" 27 | dumped_json: str = dump_to_json(data) 28 | assert dumped_json == json 29 | assert load_from_json(dumped_json) == round_trip 30 | -------------------------------------------------------------------------------- /key-value/key-value-shared-test/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "py-key-value-shared-test" 3 | version = "0.3.0" 4 | description = "Shared Key-Value Test" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | classifiers = [ 8 | "Development Status :: 3 - Alpha", 9 | "Intended Audience :: Developers", 10 | "License :: OSI Approved :: Apache Software License", 11 | "Programming Language :: Python :: 3", 12 | "Programming Language :: Python :: 3.10", 13 | "Programming Language :: Python :: 3.11", 14 | "Programming Language :: Python :: 3.12", 15 | "Programming Language :: Python :: 3.13", 16 | ] 17 | 18 | 19 | 20 | [build-system] 21 | requires = ["uv_build>=0.8.2,<0.9.0"] 22 | build-backend = "uv_build" 23 | 24 | [tool.uv.build-backend] 25 | module-name = "key_value.shared_test" 26 | 27 | [dependency-groups] 28 | dev = [ 29 | "basedpyright>=1.32.1", 30 | "dirty-equals>=0.10.0", 31 | "docker>=7.1.0", 32 | "inline-snapshot>=0.30.1", 33 | "pytest-asyncio>=1.2.0", 34 | "pytest-dotenv>=0.5.2", 35 | "pytest-mock>=3.15.1", 36 | "pytest-timeout>=2.4.0", 37 | "pytest-xdist>=3.8.0", 38 | "pytest>=8.4.2", 39 | "ruff>=0.14.2", 40 | ] 41 | 42 | [tool.ruff] 43 | extend="../../pyproject.toml" 44 | 45 | [tool.pyright] 46 | extends = "../../pyproject.toml" 47 | 48 | executionEnvironments = [ 49 | { root = "tests", reportPrivateUsage = false, extraPaths = ["src"]}, 50 | { root = "src" } 51 | ] -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/filetree/test_filetree.py: -------------------------------------------------------------------------------- 1 | """Tests for FileTreeStore.""" 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | from typing_extensions import override 7 | 8 | from key_value.aio.stores.base import BaseStore 9 | from key_value.aio.stores.filetree import ( 10 | FileTreeStore, 11 | FileTreeV1CollectionSanitizationStrategy, 12 | FileTreeV1KeySanitizationStrategy, 13 | ) 14 | from tests.stores.base import BaseStoreTests 15 | 16 | 17 | class TestFileTreeStore(BaseStoreTests): 18 | """Test suite for FileTreeStore.""" 19 | 20 | @pytest.fixture 21 | async def store(self, per_test_temp_dir: Path) -> FileTreeStore: 22 | """Create a FileTreeStore instance with a temporary directory. 23 | 24 | Uses V1 sanitization strategies to maintain backwards compatibility 25 | and pass tests that rely on sanitization for long/special names. 26 | """ 27 | return FileTreeStore( 28 | data_directory=per_test_temp_dir, 29 | key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(directory=per_test_temp_dir), 30 | collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(directory=per_test_temp_dir), 31 | ) 32 | 33 | @override 34 | async def test_not_unbounded(self, store: BaseStore): 35 | """FileTreeStore is unbounded, so skip this test.""" 36 | pytest.skip("FileTreeStore is unbounded and does not evict old entries") 37 | -------------------------------------------------------------------------------- /key-value/key-value-shared/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "py-key-value-shared" 3 | version = "0.3.0" 4 | description = "Shared Key-Value" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | classifiers = [ 8 | "Development Status :: 3 - Alpha", 9 | "Intended Audience :: Developers", 10 | "License :: OSI Approved :: Apache Software License", 11 | "Programming Language :: Python :: 3", 12 | "Programming Language :: Python :: 3.10", 13 | "Programming Language :: Python :: 3.11", 14 | "Programming Language :: Python :: 3.12", 15 | "Programming Language :: Python :: 3.13", 16 | ] 17 | dependencies = [ 18 | "typing-extensions>=4.15.0", 19 | "beartype>=0.20.0", 20 | ] 21 | 22 | 23 | 24 | [build-system] 25 | requires = ["uv_build>=0.8.2,<0.9.0"] 26 | build-backend = "uv_build" 27 | 28 | [tool.uv.build-backend] 29 | module-name = "key_value.shared" 30 | 31 | [tool.pytest.ini_options] 32 | asyncio_mode = "auto" 33 | addopts = [ 34 | "--inline-snapshot=disable", 35 | "-vv", 36 | "-n=auto", 37 | "--dist=loadfile", 38 | ] 39 | markers = [ 40 | "skip_on_ci: Skip running the test when running on CI", 41 | ] 42 | timeout = 10 43 | 44 | env_files = [".env"] 45 | 46 | [dependency-groups] 47 | dev = [ 48 | "py-key-value[dev]", 49 | ] 50 | 51 | [tool.uv.sources] 52 | py-key-value-shared-test = { workspace = true } 53 | 54 | [tool.ruff] 55 | extend="../../pyproject.toml" 56 | 57 | [tool.pyright] 58 | extends = "../../pyproject.toml" 59 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/wrappers/test_passthrough_cache.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_passthrough_cache.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import tempfile 5 | from collections.abc import Generator 6 | 7 | import pytest 8 | from typing_extensions import override 9 | 10 | from key_value.sync.code_gen.stores.disk.store import DiskStore 11 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 12 | from key_value.sync.code_gen.wrappers.passthrough_cache import PassthroughCacheWrapper 13 | from tests.code_gen.stores.base import BaseStoreTests 14 | 15 | DISK_STORE_SIZE_LIMIT = 100 * 1024 # 100KB 16 | 17 | 18 | class TestPassthroughCacheWrapper(BaseStoreTests): 19 | @pytest.fixture(scope="session") 20 | def primary_store(self) -> Generator[DiskStore, None, None]: 21 | with tempfile.TemporaryDirectory() as temp_dir, DiskStore(directory=temp_dir, max_size=DISK_STORE_SIZE_LIMIT) as disk_store: 22 | yield disk_store 23 | 24 | @pytest.fixture 25 | def cache_store(self, memory_store: MemoryStore) -> MemoryStore: 26 | return memory_store 27 | 28 | @override 29 | @pytest.fixture 30 | def store(self, primary_store: DiskStore, cache_store: MemoryStore) -> PassthroughCacheWrapper: 31 | primary_store._cache.clear() # pyright: ignore[reportPrivateUsage] 32 | return PassthroughCacheWrapper(primary_key_value=primary_store, cache_key_value=cache_store) 33 | -------------------------------------------------------------------------------- /key-value/key-value-shared/tests/utils/test_time_to_live.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import datetime, timezone 3 | from typing import Any 4 | 5 | import pytest 6 | 7 | from key_value.shared.errors.key_value import InvalidTTLError 8 | from key_value.shared.utils.time_to_live import prepare_ttl 9 | 10 | FIXED_DATETIME = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc) 11 | 12 | 13 | @pytest.mark.parametrize( 14 | ("t", "expected"), 15 | [ 16 | (100, 100), 17 | (None, None), 18 | (100.0, 100.0), 19 | (0.1, 0.1), 20 | (sys.maxsize, float(sys.maxsize)), 21 | ], 22 | ids=["int", "none", "float", "float-0.1", "int-maxsize"], 23 | ) 24 | def test_prepare_ttl(t: Any, expected: int | float | None): 25 | assert prepare_ttl(t) == expected 26 | 27 | 28 | @pytest.mark.parametrize( 29 | ("t"), 30 | [ 31 | "100", 32 | "None", 33 | "-100", 34 | "-None", 35 | "0.1", 36 | FIXED_DATETIME, 37 | FIXED_DATETIME.isoformat(), 38 | object(), 39 | {}, 40 | True, 41 | False, 42 | ], 43 | ids=[ 44 | "string", 45 | "string-none", 46 | "string-negative-int", 47 | "string-negative-none", 48 | "string-float", 49 | "datetime", 50 | "datetime-isoformat", 51 | "object", 52 | "dict", 53 | "bool-true", 54 | "bool-false", 55 | ], 56 | ) 57 | @pytest.mark.filterwarnings("ignore:Function key_value.shared.utils") # Ignore BearType warnings here 58 | def test_prepare_ttl_invalid(t: Any): 59 | with pytest.raises(InvalidTTLError): 60 | prepare_ttl(t) 61 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | push: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | 16 | strategy: 17 | matrix: 18 | project: 19 | - "key-value/key-value-aio" 20 | - "key-value/key-value-shared" 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: "Install uv" 26 | uses: astral-sh/setup-uv@v6 27 | 28 | - name: "Install" 29 | run: uv sync --locked --group dev 30 | working-directory: ${{ matrix.project }} 31 | 32 | - name: "Test" 33 | run: uv run pytest tests 34 | working-directory: ${{ matrix.project }} 35 | 36 | publish: 37 | needs: test 38 | runs-on: ubuntu-latest 39 | timeout-minutes: 10 40 | permissions: 41 | id-token: write 42 | environment: pypi 43 | 44 | strategy: 45 | matrix: 46 | project: 47 | - "key-value/key-value-aio" 48 | - "key-value/key-value-shared" 49 | steps: 50 | - name: Checkout repository 51 | uses: actions/checkout@v4 52 | 53 | - name: "Install uv" 54 | uses: astral-sh/setup-uv@v6 55 | 56 | - name: "Install" 57 | run: uv sync --locked --group dev 58 | working-directory: ${{ matrix.project }} 59 | 60 | - name: "Build" 61 | run: uv build 62 | working-directory: ${{ matrix.project }} 63 | 64 | - name: "Publish to PyPi" 65 | if: github.event_name == 'release' && github.event.action == 'created' 66 | run: uv publish -v dist/* 67 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | Complete API reference documentation for py-key-value. 4 | 5 | ## Overview 6 | 7 | The py-key-value API is organized into four main components: 8 | 9 | - **[Protocols](protocols.md)** - Core interfaces for the key-value store 10 | - **[Stores](stores.md)** - Backend implementations for different storage systems 11 | - **[Wrappers](wrappers.md)** - Decorators that add functionality to stores 12 | - **[Adapters](adapters.md)** - Utilities that simplify working with stores 13 | 14 | ## Quick Links 15 | 16 | ### Core Protocols 17 | 18 | The [`AsyncKeyValue`](protocols.md) protocol defines the async interface that all 19 | stores implement. 20 | 21 | The [`KeyValue`](protocols.md) protocol is the synchronous version. 22 | 23 | ### Popular Stores 24 | 25 | - [MemoryStore](stores.md) - In-memory storage 26 | - [RedisStore](stores.md) - Redis backend 27 | - [DiskStore](stores.md) - File-based storage 28 | 29 | ### Common Wrappers 30 | 31 | - [LoggingWrapper](wrappers.md) - Add logging to any store 32 | - [CacheWrapper](wrappers.md) - Add caching layer 33 | - [RetryWrapper](wrappers.md) - Add automatic retry logic 34 | 35 | ## Using the API Reference 36 | 37 | Each page provides: 38 | 39 | - **Type signatures** - Full type information for all parameters and return values 40 | - **Docstrings** - Detailed descriptions of functionality 41 | - **Source links** - View the implementation on GitHub 42 | - **Cross-references** - Navigate between related components 43 | 44 | ## Example Usage 45 | 46 | ```python 47 | from key_value.aio.stores.memory import MemoryStore 48 | from key_value.aio.wrappers.logging import LoggingWrapper 49 | 50 | # Create a store with logging 51 | store = LoggingWrapper(MemoryStore()) 52 | 53 | # Use the store 54 | await store.put("key", "value") 55 | result = await store.get("key") 56 | ``` 57 | 58 | For more examples and guides, see the [User Guide](../stores.md). 59 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Python Debugger: Current File", 10 | "type": "debugpy", 11 | "request": "launch", 12 | "program": "${file}", 13 | "console": "integratedTerminal" 14 | },{ 15 | "name": "Python: Debug Tests", 16 | "type": "debugpy", 17 | "request": "launch", 18 | "program": "${file}", 19 | "purpose": [ 20 | "debug-test" 21 | ], 22 | "args": [ 23 | "-vv", 24 | "-s" // Disable Captures 25 | ], 26 | "console": "integratedTerminal", 27 | "justMyCode": false, 28 | "envFile": "${workspaceFolder}/.env" 29 | }, 30 | { 31 | "name": "Compile Sync Library", 32 | "type": "debugpy", 33 | "request": "launch", 34 | "program": "${workspaceFolder}/scripts/build_sync_library.py", 35 | "console": "integratedTerminal", 36 | "justMyCode": false, 37 | "envFile": "${workspaceFolder}/.env", 38 | "args": [] 39 | }, 40 | { 41 | "name": "Compile Sync Library - Single File", 42 | "type": "debugpy", 43 | "request": "launch", 44 | "program": "${workspaceFolder}/scripts/build_sync_library.py", 45 | "console": "integratedTerminal", 46 | "justMyCode": false, 47 | "envFile": "${workspaceFolder}/.env", 48 | "args": ["${workspaceFolder}/key-value/key-value-aio/src/key_value/aio/stores/memcached/__init__.py"] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/errors/key_value.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from key_value.shared.errors.base import BaseKeyValueError, ExtraInfoType 4 | 5 | 6 | class KeyValueOperationError(BaseKeyValueError): 7 | """Base exception for all Key-Value operation errors.""" 8 | 9 | 10 | class SerializationError(KeyValueOperationError): 11 | """Raised when data cannot be serialized for storage.""" 12 | 13 | 14 | class DeserializationError(KeyValueOperationError): 15 | """Raised when stored data cannot be deserialized back to its original form.""" 16 | 17 | 18 | class MissingKeyError(KeyValueOperationError): 19 | """Raised when a key is missing from the store.""" 20 | 21 | def __init__(self, operation: str, collection: str | None = None, key: str | None = None): 22 | super().__init__( 23 | message="A key was requested that was required but not found in the store.", 24 | extra_info={"operation": operation, "collection": collection or "default", "key": key}, 25 | ) 26 | 27 | 28 | class InvalidTTLError(KeyValueOperationError): 29 | """Raised when a TTL is invalid.""" 30 | 31 | def __init__(self, ttl: Any, extra_info: ExtraInfoType | None = None): 32 | super().__init__( 33 | message="A TTL is invalid.", 34 | extra_info={"ttl": str(ttl), **(extra_info or {})}, 35 | ) 36 | 37 | 38 | class InvalidKeyError(KeyValueOperationError): 39 | """Raised when a key is invalid (e.g., uses reserved prefixes).""" 40 | 41 | 42 | class ValueTooLargeError(KeyValueOperationError): 43 | """Raised when a value is too large.""" 44 | 45 | def __init__(self, size: int, max_size: int, collection: str | None = None, key: str | None = None): 46 | super().__init__( 47 | message="Value size exceeds the maximum allowed size.", 48 | extra_info={"size": size, "max_size": max_size, "collection": collection or "default", "key": key}, 49 | ) 50 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/routing/collection_routing.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from types import MappingProxyType 3 | 4 | from key_value.aio.protocols.key_value import AsyncKeyValue 5 | from key_value.aio.wrappers.routing.wrapper import RoutingWrapper 6 | 7 | 8 | class CollectionRoutingWrapper(RoutingWrapper): 9 | """Routes operations based on collection name using a simple map. 10 | 11 | This is a convenience wrapper that provides collection-based routing using a 12 | dictionary mapping collection names to stores. This is useful for directing 13 | different data types to different backing stores. 14 | 15 | Example: 16 | router = CollectionRoutingWrapper( 17 | collection_map={ 18 | "sessions": redis_store, 19 | "users": dynamo_store, 20 | "cache": memory_store, 21 | }, 22 | default_store=disk_store 23 | ) 24 | """ 25 | 26 | _collection_map: MappingProxyType[str, AsyncKeyValue] 27 | 28 | def __init__( 29 | self, 30 | collection_map: Mapping[str, AsyncKeyValue], 31 | default_store: AsyncKeyValue, 32 | ) -> None: 33 | """Initialize collection-based routing. 34 | 35 | Args: 36 | collection_map: Mapping from collection name to store. Each collection 37 | name is mapped to its corresponding backing store. 38 | default_store: Store to use for unmapped collections. 39 | """ 40 | self._collection_map = MappingProxyType(mapping=dict(collection_map)) 41 | 42 | def route_by_collection(collection: str | None) -> AsyncKeyValue | None: 43 | if collection is not None: 44 | return self._collection_map.get(collection) 45 | 46 | return None 47 | 48 | super().__init__( 49 | routing_function=route_by_collection, 50 | default_store=default_store, 51 | ) 52 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/routing/collection_routing.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'collection_routing.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from collections.abc import Mapping 5 | from types import MappingProxyType 6 | 7 | from key_value.sync.code_gen.protocols.key_value import KeyValue 8 | from key_value.sync.code_gen.wrappers.routing.wrapper import RoutingWrapper 9 | 10 | 11 | class CollectionRoutingWrapper(RoutingWrapper): 12 | """Routes operations based on collection name using a simple map. 13 | 14 | This is a convenience wrapper that provides collection-based routing using a 15 | dictionary mapping collection names to stores. This is useful for directing 16 | different data types to different backing stores. 17 | 18 | Example: 19 | router = CollectionRoutingWrapper( 20 | collection_map={ 21 | "sessions": redis_store, 22 | "users": dynamo_store, 23 | "cache": memory_store, 24 | }, 25 | default_store=disk_store 26 | ) 27 | """ 28 | 29 | _collection_map: MappingProxyType[str, KeyValue] 30 | 31 | def __init__(self, collection_map: Mapping[str, KeyValue], default_store: KeyValue) -> None: 32 | """Initialize collection-based routing. 33 | 34 | Args: 35 | collection_map: Mapping from collection name to store. Each collection 36 | name is mapped to its corresponding backing store. 37 | default_store: Store to use for unmapped collections. 38 | """ 39 | self._collection_map = MappingProxyType(mapping=dict(collection_map)) 40 | 41 | def route_by_collection(collection: str | None) -> KeyValue | None: 42 | if collection is not None: 43 | return self._collection_map.get(collection) 44 | 45 | return None 46 | 47 | super().__init__(routing_function=route_by_collection, default_store=default_store) 48 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/cases.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from typing import Any 3 | 4 | FIXED_DATETIME = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc) 5 | FIXED_TIME = FIXED_DATETIME.time() 6 | 7 | LARGE_STRING: str = "a" * 10000 # 10KB 8 | LARGE_INT: int = 1 * 10**18 # 18 digits 9 | LARGE_FLOAT: float = 1.0 * 10**63 # 63 digits 10 | 11 | SIMPLE_CASE: dict[str, Any] = { 12 | "key_1": "value_1", 13 | "key_2": 1, 14 | "key_3": 1.0, 15 | "key_4": [1, 2, 3], 16 | "key_5": {"nested": "value"}, 17 | "key_6": True, 18 | "key_7": False, 19 | "key_8": None, 20 | } 21 | 22 | SIMPLE_CASE_JSON: str = '{"key_1": "value_1", "key_2": 1, "key_3": 1.0, "key_4": [1, 2, 3], "key_5": {"nested": "value"}, "key_6": true, "key_7": false, "key_8": null}' 23 | 24 | DICTIONARY_TO_JSON_TEST_CASES: list[tuple[dict[str, Any], str]] = [ 25 | ({"key": "value"}, '{"key": "value"}'), 26 | ({"key": 1}, '{"key": 1}'), 27 | ({"key": 1.0}, '{"key": 1.0}'), 28 | ({"key": [1, 2, 3]}, '{"key": [1, 2, 3]}'), 29 | # ({"key": (1, 2, 3)}, '{"key": [1, 2, 3]}'), 30 | ({"key": {"nested": "value"}}, '{"key": {"nested": "value"}}'), 31 | ({"key": True}, '{"key": true}'), 32 | ({"key": False}, '{"key": false}'), 33 | ({"key": None}, '{"key": null}'), 34 | ( 35 | {"key": {"int": 1, "float": 1.0, "list": [1, 2, 3], "dict": {"nested": "value"}, "bool": True, "null": None}}, 36 | '{"key": {"int": 1, "float": 1.0, "list": [1, 2, 3], "dict": {"nested": "value"}, "bool": true, "null": null}}', 37 | ), 38 | ({"key": LARGE_STRING}, f'{{"key": "{LARGE_STRING}"}}'), 39 | ({"key": LARGE_INT}, f'{{"key": {LARGE_INT}}}'), 40 | ({"key": LARGE_FLOAT}, f'{{"key": {LARGE_FLOAT}}}'), 41 | ] 42 | 43 | DICTIONARY_TO_JSON_TEST_CASES_NAMES: list[str] = [ 44 | "string", 45 | "int", 46 | "float", 47 | "list", 48 | # "tuple", 49 | "dict", 50 | "bool-true", 51 | "bool-false", 52 | "null", 53 | "dict-nested", 54 | "large-string", 55 | "large-int", 56 | "large-float", 57 | ] 58 | 59 | OBJECT_TEST_CASES: list[dict[str, Any]] = [test_case[0] for test_case in DICTIONARY_TO_JSON_TEST_CASES] 60 | 61 | JSON_TEST_CASES: list[str] = [test_case[1] for test_case in DICTIONARY_TO_JSON_TEST_CASES] 62 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/disk/test_disk.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import pytest 5 | from dirty_equals import IsDatetime 6 | from diskcache.core import Cache 7 | from inline_snapshot import snapshot 8 | from typing_extensions import override 9 | 10 | from key_value.aio.stores.disk import DiskStore 11 | from tests.stores.base import BaseStoreTests, ContextManagerStoreTestMixin 12 | 13 | TEST_SIZE_LIMIT = 100 * 1024 # 100KB 14 | 15 | 16 | class TestDiskStore(ContextManagerStoreTestMixin, BaseStoreTests): 17 | @override 18 | @pytest.fixture 19 | async def store(self, per_test_temp_dir: Path) -> DiskStore: 20 | disk_store = DiskStore(directory=per_test_temp_dir, max_size=TEST_SIZE_LIMIT) 21 | 22 | disk_store._cache.clear() # pyright: ignore[reportPrivateUsage] 23 | 24 | return disk_store 25 | 26 | @pytest.fixture 27 | async def disk_cache(self, store: DiskStore) -> Cache: 28 | assert isinstance(store._cache, Cache) 29 | return store._cache # pyright: ignore[reportPrivateUsage] 30 | 31 | async def test_value_stored(self, store: DiskStore, disk_cache: Cache): 32 | await store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30}) 33 | 34 | value = disk_cache.get(key="test::test_key") 35 | value_as_dict = json.loads(value) 36 | assert value_as_dict == snapshot( 37 | { 38 | "collection": "test", 39 | "created_at": IsDatetime(iso_string=True), 40 | "key": "test_key", 41 | "value": {"age": 30, "name": "Alice"}, 42 | "version": 1, 43 | } 44 | ) 45 | 46 | await store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30}, ttl=10) 47 | 48 | value = disk_cache.get(key="test::test_key") 49 | value_as_dict = json.loads(value) 50 | assert value_as_dict == snapshot( 51 | { 52 | "collection": "test", 53 | "created_at": IsDatetime(iso_string=True), 54 | "value": {"age": 30, "name": "Alice"}, 55 | "key": "test_key", 56 | "expires_at": IsDatetime(iso_string=True), 57 | "version": 1, 58 | } 59 | ) 60 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_ttl_clamp.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from dirty_equals import IsFloat 3 | from typing_extensions import override 4 | 5 | from key_value.aio.stores.memory.store import MemoryStore 6 | from key_value.aio.wrappers.ttl_clamp import TTLClampWrapper 7 | from tests.stores.base import BaseStoreTests 8 | 9 | 10 | class TestTTLClampWrapper(BaseStoreTests): 11 | @override 12 | @pytest.fixture 13 | async def store(self, memory_store: MemoryStore) -> TTLClampWrapper: 14 | return TTLClampWrapper(key_value=memory_store, min_ttl=0, max_ttl=100) 15 | 16 | async def test_put_below_min_ttl(self, memory_store: MemoryStore): 17 | ttl_clamp_store: TTLClampWrapper = TTLClampWrapper(key_value=memory_store, min_ttl=50, max_ttl=100) 18 | 19 | await ttl_clamp_store.put(collection="test", key="test", value={"test": "test"}, ttl=5) 20 | assert await ttl_clamp_store.get(collection="test", key="test") is not None 21 | 22 | value, ttl = await ttl_clamp_store.ttl(collection="test", key="test") 23 | assert value is not None 24 | assert ttl is not None 25 | assert ttl == IsFloat(approx=50) 26 | 27 | async def test_put_above_max_ttl(self, memory_store: MemoryStore): 28 | ttl_clamp_store: TTLClampWrapper = TTLClampWrapper(key_value=memory_store, min_ttl=0, max_ttl=100) 29 | 30 | await ttl_clamp_store.put(collection="test", key="test", value={"test": "test"}, ttl=1000) 31 | assert await ttl_clamp_store.get(collection="test", key="test") is not None 32 | 33 | value, ttl = await ttl_clamp_store.ttl(collection="test", key="test") 34 | assert value is not None 35 | assert ttl is not None 36 | assert ttl == IsFloat(approx=100) 37 | 38 | async def test_put_missing_ttl(self, memory_store: MemoryStore): 39 | ttl_clamp_store: TTLClampWrapper = TTLClampWrapper(key_value=memory_store, min_ttl=0, max_ttl=100, missing_ttl=50) 40 | 41 | await ttl_clamp_store.put(collection="test", key="test", value={"test": "test"}, ttl=None) 42 | assert await ttl_clamp_store.get(collection="test", key="test") is not None 43 | 44 | value, ttl = await ttl_clamp_store.ttl(collection="test", key="test") 45 | assert value is not None 46 | assert ttl is not None 47 | 48 | assert ttl == IsFloat(approx=50) 49 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/cases.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'cases.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from datetime import datetime, timezone 5 | from typing import Any 6 | 7 | FIXED_DATETIME = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc) 8 | FIXED_TIME = FIXED_DATETIME.time() 9 | 10 | LARGE_STRING: str = "a" * 10000 # 10KB 11 | LARGE_INT: int = 1 * 10**18 # 18 digits 12 | LARGE_FLOAT: float = 1.0 * 10**63 # 63 digits 13 | 14 | SIMPLE_CASE: dict[str, Any] = { 15 | "key_1": "value_1", 16 | "key_2": 1, 17 | "key_3": 1.0, 18 | "key_4": [1, 2, 3], 19 | "key_5": {"nested": "value"}, 20 | "key_6": True, 21 | "key_7": False, 22 | "key_8": None, 23 | } 24 | 25 | SIMPLE_CASE_JSON: str = '{"key_1": "value_1", "key_2": 1, "key_3": 1.0, "key_4": [1, 2, 3], "key_5": {"nested": "value"}, "key_6": true, "key_7": false, "key_8": null}' 26 | 27 | # ({"key": (1, 2, 3)}, '{"key": [1, 2, 3]}'), 28 | DICTIONARY_TO_JSON_TEST_CASES: list[tuple[dict[str, Any], str]] = [ 29 | ({"key": "value"}, '{"key": "value"}'), 30 | ({"key": 1}, '{"key": 1}'), 31 | ({"key": 1.0}, '{"key": 1.0}'), 32 | ({"key": [1, 2, 3]}, '{"key": [1, 2, 3]}'), 33 | ({"key": {"nested": "value"}}, '{"key": {"nested": "value"}}'), 34 | ({"key": True}, '{"key": true}'), 35 | ({"key": False}, '{"key": false}'), 36 | ({"key": None}, '{"key": null}'), 37 | ( 38 | {"key": {"int": 1, "float": 1.0, "list": [1, 2, 3], "dict": {"nested": "value"}, "bool": True, "null": None}}, 39 | '{"key": {"int": 1, "float": 1.0, "list": [1, 2, 3], "dict": {"nested": "value"}, "bool": true, "null": null}}', 40 | ), 41 | ({"key": LARGE_STRING}, f'{{"key": "{LARGE_STRING}"}}'), 42 | ({"key": LARGE_INT}, f'{{"key": {LARGE_INT}}}'), 43 | ({"key": LARGE_FLOAT}, f'{{"key": {LARGE_FLOAT}}}'), 44 | ] 45 | 46 | # "tuple", 47 | DICTIONARY_TO_JSON_TEST_CASES_NAMES: list[str] = [ 48 | "string", 49 | "int", 50 | "float", 51 | "list", 52 | "dict", 53 | "bool-true", 54 | "bool-false", 55 | "null", 56 | "dict-nested", 57 | "large-string", 58 | "large-int", 59 | "large-float", 60 | ] 61 | 62 | OBJECT_TEST_CASES: list[dict[str, Any]] = [test_case[0] for test_case in DICTIONARY_TO_JSON_TEST_CASES] 63 | 64 | JSON_TEST_CASES: list[str] = [test_case[1] for test_case in DICTIONARY_TO_JSON_TEST_CASES] 65 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/disk/test_multi_disk.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections.abc import AsyncGenerator 3 | from pathlib import Path 4 | from typing import TYPE_CHECKING 5 | 6 | import pytest 7 | from dirty_equals import IsDatetime 8 | from inline_snapshot import snapshot 9 | from typing_extensions import override 10 | 11 | from key_value.aio.stores.disk.multi_store import MultiDiskStore 12 | from tests.stores.base import BaseStoreTests, ContextManagerStoreTestMixin 13 | 14 | if TYPE_CHECKING: 15 | from diskcache.core import Cache 16 | 17 | TEST_SIZE_LIMIT = 100 * 1024 # 100KB 18 | 19 | 20 | class TestMultiDiskStore(ContextManagerStoreTestMixin, BaseStoreTests): 21 | @override 22 | @pytest.fixture 23 | async def store(self, per_test_temp_dir: Path) -> AsyncGenerator[MultiDiskStore, None]: 24 | store = MultiDiskStore(base_directory=per_test_temp_dir, max_size=TEST_SIZE_LIMIT) 25 | 26 | yield store 27 | 28 | # Wipe the store after returning it 29 | for collection in store._cache: # pyright: ignore[reportPrivateUsage] 30 | store._cache[collection].clear() # pyright: ignore[reportPrivateUsage] 31 | 32 | async def test_value_stored(self, store: MultiDiskStore): 33 | await store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30}) 34 | disk_cache: Cache = store._cache["test"] # pyright: ignore[reportPrivateUsage] 35 | 36 | value = disk_cache.get(key="test_key") 37 | value_as_dict = json.loads(value) 38 | assert value_as_dict == snapshot( 39 | { 40 | "collection": "test", 41 | "value": {"name": "Alice", "age": 30}, 42 | "key": "test_key", 43 | "created_at": IsDatetime(iso_string=True), 44 | "version": 1, 45 | } 46 | ) 47 | 48 | await store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30}, ttl=10) 49 | 50 | value = disk_cache.get(key="test_key") 51 | value_as_dict = json.loads(value) 52 | assert value_as_dict == snapshot( 53 | { 54 | "collection": "test", 55 | "created_at": IsDatetime(iso_string=True), 56 | "value": {"age": 30, "name": "Alice"}, 57 | "key": "test_key", 58 | "expires_at": IsDatetime(iso_string=True), 59 | "version": 1, 60 | } 61 | ) 62 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/disk/test_disk.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_disk.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import json 5 | from pathlib import Path 6 | 7 | import pytest 8 | from dirty_equals import IsDatetime 9 | from diskcache.core import Cache 10 | from inline_snapshot import snapshot 11 | from typing_extensions import override 12 | 13 | from key_value.sync.code_gen.stores.disk import DiskStore 14 | from tests.code_gen.stores.base import BaseStoreTests, ContextManagerStoreTestMixin 15 | 16 | TEST_SIZE_LIMIT = 100 * 1024 # 100KB 17 | 18 | 19 | class TestDiskStore(ContextManagerStoreTestMixin, BaseStoreTests): 20 | @override 21 | @pytest.fixture 22 | def store(self, per_test_temp_dir: Path) -> DiskStore: 23 | disk_store = DiskStore(directory=per_test_temp_dir, max_size=TEST_SIZE_LIMIT) 24 | 25 | disk_store._cache.clear() # pyright: ignore[reportPrivateUsage] 26 | 27 | return disk_store 28 | 29 | @pytest.fixture 30 | def disk_cache(self, store: DiskStore) -> Cache: 31 | assert isinstance(store._cache, Cache) 32 | return store._cache # pyright: ignore[reportPrivateUsage] 33 | 34 | def test_value_stored(self, store: DiskStore, disk_cache: Cache): 35 | store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30}) 36 | 37 | value = disk_cache.get(key="test::test_key") 38 | value_as_dict = json.loads(value) 39 | assert value_as_dict == snapshot( 40 | { 41 | "collection": "test", 42 | "created_at": IsDatetime(iso_string=True), 43 | "key": "test_key", 44 | "value": {"age": 30, "name": "Alice"}, 45 | "version": 1, 46 | } 47 | ) 48 | 49 | store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30}, ttl=10) 50 | 51 | value = disk_cache.get(key="test::test_key") 52 | value_as_dict = json.loads(value) 53 | assert value_as_dict == snapshot( 54 | { 55 | "collection": "test", 56 | "created_at": IsDatetime(iso_string=True), 57 | "value": {"age": 30, "name": "Alice"}, 58 | "key": "test_key", 59 | "expires_at": IsDatetime(iso_string=True), 60 | "version": 1, 61 | } 62 | ) 63 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/wrappers/test_ttl_clamp.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_ttl_clamp.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from dirty_equals import IsFloat 6 | from typing_extensions import override 7 | 8 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 9 | from key_value.sync.code_gen.wrappers.ttl_clamp import TTLClampWrapper 10 | from tests.code_gen.stores.base import BaseStoreTests 11 | 12 | 13 | class TestTTLClampWrapper(BaseStoreTests): 14 | @override 15 | @pytest.fixture 16 | def store(self, memory_store: MemoryStore) -> TTLClampWrapper: 17 | return TTLClampWrapper(key_value=memory_store, min_ttl=0, max_ttl=100) 18 | 19 | def test_put_below_min_ttl(self, memory_store: MemoryStore): 20 | ttl_clamp_store: TTLClampWrapper = TTLClampWrapper(key_value=memory_store, min_ttl=50, max_ttl=100) 21 | 22 | ttl_clamp_store.put(collection="test", key="test", value={"test": "test"}, ttl=5) 23 | assert ttl_clamp_store.get(collection="test", key="test") is not None 24 | 25 | (value, ttl) = ttl_clamp_store.ttl(collection="test", key="test") 26 | assert value is not None 27 | assert ttl is not None 28 | assert ttl == IsFloat(approx=50) 29 | 30 | def test_put_above_max_ttl(self, memory_store: MemoryStore): 31 | ttl_clamp_store: TTLClampWrapper = TTLClampWrapper(key_value=memory_store, min_ttl=0, max_ttl=100) 32 | 33 | ttl_clamp_store.put(collection="test", key="test", value={"test": "test"}, ttl=1000) 34 | assert ttl_clamp_store.get(collection="test", key="test") is not None 35 | 36 | (value, ttl) = ttl_clamp_store.ttl(collection="test", key="test") 37 | assert value is not None 38 | assert ttl is not None 39 | assert ttl == IsFloat(approx=100) 40 | 41 | def test_put_missing_ttl(self, memory_store: MemoryStore): 42 | ttl_clamp_store: TTLClampWrapper = TTLClampWrapper(key_value=memory_store, min_ttl=0, max_ttl=100, missing_ttl=50) 43 | 44 | ttl_clamp_store.put(collection="test", key="test", value={"test": "test"}, ttl=None) 45 | assert ttl_clamp_store.get(collection="test", key="test") is not None 46 | 47 | (value, ttl) = ttl_clamp_store.ttl(collection="test", key="test") 48 | assert value is not None 49 | assert ttl is not None 50 | 51 | assert ttl == IsFloat(approx=50) 52 | -------------------------------------------------------------------------------- /docs/api/stores.md: -------------------------------------------------------------------------------- 1 | # Stores 2 | 3 | Stores are implementations of the `AsyncKeyValue` protocol that provide actual 4 | storage backends. 5 | 6 | ## Memory Store 7 | 8 | In-memory key-value store, useful for testing and development. 9 | 10 | ::: key_value.aio.stores.memory.MemoryStore 11 | options: 12 | show_source: false 13 | members: 14 | - __init__ 15 | 16 | ## Disk Store 17 | 18 | Persistent disk-based key-value store using DiskCache. 19 | 20 | ::: key_value.aio.stores.disk.DiskStore 21 | options: 22 | show_source: false 23 | members: 24 | - __init__ 25 | 26 | ## FileTree Store 27 | 28 | Directory-based store for visual inspection and testing. 29 | 30 | ::: key_value.aio.stores.filetree.FileTreeStore 31 | options: 32 | show_source: false 33 | members: 34 | - __init__ 35 | 36 | ## Redis Store 37 | 38 | Redis-backed key-value store. 39 | 40 | ::: key_value.aio.stores.redis.RedisStore 41 | options: 42 | show_source: false 43 | members: 44 | - __init__ 45 | 46 | ## DynamoDB Store 47 | 48 | AWS DynamoDB-backed key-value store. 49 | 50 | ::: key_value.aio.stores.dynamodb.DynamoDBStore 51 | options: 52 | show_source: false 53 | members: 54 | - __init__ 55 | 56 | ## Elasticsearch Store 57 | 58 | Elasticsearch-backed key-value store. 59 | 60 | ::: key_value.aio.stores.elasticsearch.ElasticsearchStore 61 | options: 62 | show_source: false 63 | members: 64 | - __init__ 65 | 66 | ## MongoDB Store 67 | 68 | MongoDB-backed key-value store. 69 | 70 | ::: key_value.aio.stores.mongodb.MongoDBStore 71 | options: 72 | show_source: false 73 | members: 74 | - __init__ 75 | 76 | ## Valkey Store 77 | 78 | Valkey-backed key-value store (Redis-compatible). 79 | 80 | ::: key_value.aio.stores.valkey.ValkeyStore 81 | options: 82 | show_source: false 83 | members: 84 | - __init__ 85 | 86 | ## Memcached Store 87 | 88 | Memcached-backed key-value store. 89 | 90 | ::: key_value.aio.stores.memcached.MemcachedStore 91 | options: 92 | show_source: false 93 | members: 94 | - __init__ 95 | 96 | ## Null Store 97 | 98 | A no-op store that doesn't persist anything, useful for testing. 99 | 100 | ::: key_value.aio.stores.null.NullStore 101 | options: 102 | show_source: false 103 | members: 104 | - __init__ 105 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: py-key-value Documentation 2 | site_description: A pluggable interface for Key-Value Stores 3 | site_url: https://strawgate.github.io/py-key-value/ 4 | repo_url: https://github.com/strawgate/py-key-value 5 | repo_name: strawgate/py-key-value 6 | edit_uri: edit/main/docs/ 7 | 8 | theme: 9 | name: material 10 | palette: 11 | # Palette toggle for light mode 12 | - scheme: default 13 | primary: indigo 14 | accent: indigo 15 | toggle: 16 | icon: material/brightness-7 17 | name: Switch to dark mode 18 | # Palette toggle for dark mode 19 | - scheme: slate 20 | primary: indigo 21 | accent: indigo 22 | toggle: 23 | icon: material/brightness-4 24 | name: Switch to light mode 25 | features: 26 | - navigation.tabs 27 | - navigation.tabs.sticky 28 | - navigation.sections 29 | - navigation.expand 30 | - navigation.top 31 | - navigation.tracking 32 | - navigation.instant 33 | - navigation.path 34 | - navigation.indexes 35 | - navigation.footer 36 | - toc.follow 37 | - search.suggest 38 | - search.highlight 39 | - search.share 40 | - content.code.copy 41 | - content.code.annotate 42 | - content.action.edit 43 | - content.action.view 44 | 45 | plugins: 46 | - search 47 | - mkdocstrings: 48 | handlers: 49 | python: 50 | paths: [key-value/key-value-aio/src] 51 | options: 52 | show_source: true 53 | docstring_style: google 54 | show_root_heading: true 55 | show_root_full_path: false 56 | show_symbol_type_heading: true 57 | show_symbol_type_toc: true 58 | signature_crossrefs: true 59 | separate_signature: true 60 | line_length: 80 61 | 62 | markdown_extensions: 63 | - admonition 64 | - pymdownx.details 65 | - pymdownx.superfences 66 | - pymdownx.highlight: 67 | anchor_linenums: true 68 | - pymdownx.inlinehilite 69 | - pymdownx.snippets 70 | - pymdownx.tabbed: 71 | alternate_style: true 72 | - tables 73 | - toc: 74 | permalink: true 75 | 76 | nav: 77 | - Home: index.md 78 | - Getting Started: getting-started.md 79 | - User Guide: 80 | - Stores: stores.md 81 | - Wrappers: wrappers.md 82 | - Adapters: adapters.md 83 | - API Reference: 84 | - api/index.md 85 | - Protocols: api/protocols.md 86 | - Stores: api/stores.md 87 | - Wrappers: api/wrappers.md 88 | - Adapters: api/adapters.md 89 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from typing import TypeVar, get_origin 3 | 4 | from key_value.shared.type_checking.bear_spray import bear_spray 5 | from pydantic import BaseModel 6 | from pydantic.type_adapter import TypeAdapter 7 | 8 | from key_value.aio.adapters.pydantic.base import BasePydanticAdapter 9 | from key_value.aio.protocols.key_value import AsyncKeyValue 10 | 11 | T = TypeVar("T", bound=BaseModel | Sequence[BaseModel]) 12 | 13 | 14 | class PydanticAdapter(BasePydanticAdapter[T]): 15 | """Adapter around a KVStore-compliant Store that allows type-safe persistence of Pydantic models.""" 16 | 17 | # Beartype cannot handle the parameterized type annotation (type[T]) used here for this generic adapter. 18 | # Using @bear_spray to bypass beartype's runtime checks for this specific method. 19 | @bear_spray 20 | def __init__( 21 | self, 22 | key_value: AsyncKeyValue, 23 | pydantic_model: type[T], 24 | default_collection: str | None = None, 25 | raise_on_validation_error: bool = False, 26 | ) -> None: 27 | """Create a new PydanticAdapter. 28 | 29 | Args: 30 | key_value: The KVStore to use. 31 | pydantic_model: The Pydantic model to use. Can be a single Pydantic model or list[Pydantic model]. 32 | default_collection: The default collection to use. 33 | raise_on_validation_error: Whether to raise a DeserializationError if validation fails during reads. Otherwise, 34 | calls will return None if validation fails. 35 | 36 | Raises: 37 | TypeError: If pydantic_model is a sequence type other than list (e.g., tuple is not supported). 38 | """ 39 | self._key_value = key_value 40 | 41 | origin = get_origin(pydantic_model) 42 | self._is_list_model = origin is list 43 | 44 | # Validate that if it's a generic type, it must be a list (not tuple, etc.) 45 | if origin is not None and origin is not list: 46 | msg = f"Only list[BaseModel] is supported for sequence types, got {pydantic_model}" 47 | raise TypeError(msg) 48 | 49 | self._type_adapter = TypeAdapter[T](pydantic_model) 50 | self._default_collection = default_collection 51 | self._raise_on_validation_error = raise_on_validation_error 52 | 53 | def _get_model_type_name(self) -> str: 54 | """Return the model type name for error messages.""" 55 | return "Pydantic model" 56 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/disk/test_multi_disk.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_multi_disk.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import json 5 | from collections.abc import Generator 6 | from pathlib import Path 7 | from typing import TYPE_CHECKING 8 | 9 | import pytest 10 | from dirty_equals import IsDatetime 11 | from inline_snapshot import snapshot 12 | from typing_extensions import override 13 | 14 | from key_value.sync.code_gen.stores.disk.multi_store import MultiDiskStore 15 | from tests.code_gen.stores.base import BaseStoreTests, ContextManagerStoreTestMixin 16 | 17 | if TYPE_CHECKING: 18 | from diskcache.core import Cache 19 | 20 | TEST_SIZE_LIMIT = 100 * 1024 # 100KB 21 | 22 | 23 | class TestMultiDiskStore(ContextManagerStoreTestMixin, BaseStoreTests): 24 | @override 25 | @pytest.fixture 26 | def store(self, per_test_temp_dir: Path) -> Generator[MultiDiskStore, None, None]: 27 | store = MultiDiskStore(base_directory=per_test_temp_dir, max_size=TEST_SIZE_LIMIT) 28 | 29 | yield store 30 | 31 | # Wipe the store after returning it 32 | for collection in store._cache: # pyright: ignore[reportPrivateUsage] 33 | store._cache[collection].clear() # pyright: ignore[reportPrivateUsage] 34 | 35 | def test_value_stored(self, store: MultiDiskStore): 36 | store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30}) 37 | disk_cache: Cache = store._cache["test"] # pyright: ignore[reportPrivateUsage] 38 | 39 | value = disk_cache.get(key="test_key") 40 | value_as_dict = json.loads(value) 41 | assert value_as_dict == snapshot( 42 | { 43 | "collection": "test", 44 | "value": {"name": "Alice", "age": 30}, 45 | "key": "test_key", 46 | "created_at": IsDatetime(iso_string=True), 47 | "version": 1, 48 | } 49 | ) 50 | 51 | store.put(collection="test", key="test_key", value={"name": "Alice", "age": 30}, ttl=10) 52 | 53 | value = disk_cache.get(key="test_key") 54 | value_as_dict = json.loads(value) 55 | assert value_as_dict == snapshot( 56 | { 57 | "collection": "test", 58 | "created_at": IsDatetime(iso_string=True), 59 | "value": {"age": 30, "name": "Alice"}, 60 | "key": "test_key", 61 | "expires_at": IsDatetime(iso_string=True), 62 | "version": 1, 63 | } 64 | ) 65 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/ttl_clamp/wrapper.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping, Sequence 2 | from typing import Any, SupportsFloat, overload 3 | 4 | from key_value.shared.utils.time_to_live import prepare_ttl 5 | from typing_extensions import override 6 | 7 | from key_value.aio.protocols.key_value import AsyncKeyValue 8 | from key_value.aio.wrappers.base import BaseWrapper 9 | 10 | 11 | class TTLClampWrapper(BaseWrapper): 12 | """Wrapper that enforces a maximum TTL for puts into the store. 13 | 14 | This wrapper only modifies write operations (put, put_many). All read operations 15 | (get, get_many, ttl, ttl_many, delete, delete_many) pass through unchanged to 16 | the underlying store. 17 | """ 18 | 19 | def __init__( 20 | self, key_value: AsyncKeyValue, min_ttl: SupportsFloat, max_ttl: SupportsFloat, missing_ttl: SupportsFloat | None = None 21 | ) -> None: 22 | """Initialize the TTL clamp wrapper. 23 | 24 | Args: 25 | key_value: The store to wrap. 26 | min_ttl: The minimum TTL for puts into the store. 27 | max_ttl: The maximum TTL for puts into the store. 28 | missing_ttl: The TTL to use for entries that do not have a TTL. Defaults to None. 29 | """ 30 | self.key_value: AsyncKeyValue = key_value 31 | self.min_ttl: float = float(min_ttl) 32 | self.max_ttl: float = float(max_ttl) 33 | self.missing_ttl: float | None = float(missing_ttl) if missing_ttl is not None else None 34 | 35 | super().__init__() 36 | 37 | @overload 38 | def _ttl_clamp(self, ttl: SupportsFloat) -> float: ... 39 | 40 | @overload 41 | def _ttl_clamp(self, ttl: SupportsFloat | None) -> float | None: ... 42 | 43 | def _ttl_clamp(self, ttl: SupportsFloat | None) -> float | None: 44 | if ttl is None: 45 | return self.missing_ttl 46 | 47 | ttl = prepare_ttl(t=ttl) 48 | 49 | return max(self.min_ttl, min(ttl, self.max_ttl)) 50 | 51 | @override 52 | async def put(self, key: str, value: Mapping[str, Any], *, collection: str | None = None, ttl: SupportsFloat | None = None) -> None: 53 | await self.key_value.put(collection=collection, key=key, value=value, ttl=self._ttl_clamp(ttl=ttl)) 54 | 55 | @override 56 | async def put_many( 57 | self, 58 | keys: Sequence[str], 59 | values: Sequence[Mapping[str, Any]], 60 | *, 61 | collection: str | None = None, 62 | ttl: SupportsFloat | None = None, 63 | ) -> None: 64 | await self.key_value.put_many(keys=keys, values=values, collection=collection, ttl=self._ttl_clamp(ttl=ttl)) 65 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/windows_registry/test_windows_registry.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | import pytest 4 | from typing_extensions import override 5 | 6 | from key_value.aio.stores.base import BaseStore 7 | from tests.conftest import detect_on_windows 8 | from tests.stores.base import BaseStoreTests 9 | 10 | if TYPE_CHECKING: 11 | from key_value.aio.stores.windows_registry.store import WindowsRegistryStore 12 | 13 | TEST_REGISTRY_PATH = "software\\py-key-value-test" 14 | 15 | 16 | @pytest.mark.skipif(condition=not detect_on_windows(), reason="WindowsRegistryStore is only available on Windows") 17 | @pytest.mark.filterwarnings("ignore:A configured store is unstable and may change in a backwards incompatible way. Use at your own risk.") 18 | class TestWindowsRegistryStore(BaseStoreTests): 19 | def cleanup(self): 20 | from winreg import HKEY_CURRENT_USER 21 | 22 | from key_value.aio.stores.windows_registry.utils import delete_sub_keys 23 | 24 | delete_sub_keys(hive=HKEY_CURRENT_USER, sub_key=TEST_REGISTRY_PATH) 25 | 26 | @override 27 | @pytest.fixture 28 | async def store(self) -> "WindowsRegistryStore": 29 | from key_value.aio.stores.windows_registry.store import WindowsRegistryStore 30 | 31 | self.cleanup() 32 | 33 | return WindowsRegistryStore(registry_path=TEST_REGISTRY_PATH, hive="HKEY_CURRENT_USER") 34 | 35 | @pytest.fixture 36 | async def sanitizing_store(self): 37 | from key_value.aio.stores.windows_registry.store import ( 38 | WindowsRegistryStore, 39 | WindowsRegistryV1CollectionSanitizationStrategy, 40 | ) 41 | 42 | return WindowsRegistryStore( 43 | registry_path=TEST_REGISTRY_PATH, 44 | hive="HKEY_CURRENT_USER", 45 | collection_sanitization_strategy=WindowsRegistryV1CollectionSanitizationStrategy(), 46 | ) 47 | 48 | @override 49 | @pytest.mark.skip(reason="We do not test boundedness of registry stores") 50 | async def test_not_unbounded(self, store: BaseStore): ... 51 | 52 | @override 53 | async def test_long_collection_name(self, store: "WindowsRegistryStore", sanitizing_store: "WindowsRegistryStore"): # pyright: ignore[reportIncompatibleMethodOverride] 54 | with pytest.raises(Exception): # noqa: B017, PT011 55 | await store.put(collection="test_collection" * 100, key="test_key", value={"test": "test"}) 56 | 57 | await sanitizing_store.put(collection="test_collection" * 100, key="test_key", value={"test": "test"}) 58 | assert await sanitizing_store.get(collection="test_collection" * 100, key="test_key") == {"test": "test"} 59 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/adapter.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'adapter.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from collections.abc import Sequence 5 | from typing import TypeVar, get_origin 6 | 7 | from key_value.shared.type_checking.bear_spray import bear_spray 8 | from pydantic import BaseModel 9 | from pydantic.type_adapter import TypeAdapter 10 | 11 | from key_value.sync.code_gen.adapters.pydantic.base import BasePydanticAdapter 12 | from key_value.sync.code_gen.protocols.key_value import KeyValue 13 | 14 | T = TypeVar("T", bound=BaseModel | Sequence[BaseModel]) 15 | 16 | 17 | class PydanticAdapter(BasePydanticAdapter[T]): 18 | """Adapter around a KVStore-compliant Store that allows type-safe persistence of Pydantic models.""" 19 | 20 | # Beartype cannot handle the parameterized type annotation (type[T]) used here for this generic adapter. 21 | # Using @bear_spray to bypass beartype's runtime checks for this specific method. 22 | 23 | @bear_spray 24 | def __init__( 25 | self, key_value: KeyValue, pydantic_model: type[T], default_collection: str | None = None, raise_on_validation_error: bool = False 26 | ) -> None: 27 | """Create a new PydanticAdapter. 28 | 29 | Args: 30 | key_value: The KVStore to use. 31 | pydantic_model: The Pydantic model to use. Can be a single Pydantic model or list[Pydantic model]. 32 | default_collection: The default collection to use. 33 | raise_on_validation_error: Whether to raise a DeserializationError if validation fails during reads. Otherwise, 34 | calls will return None if validation fails. 35 | 36 | Raises: 37 | TypeError: If pydantic_model is a sequence type other than list (e.g., tuple is not supported). 38 | """ 39 | self._key_value = key_value 40 | 41 | origin = get_origin(pydantic_model) 42 | self._is_list_model = origin is list 43 | 44 | # Validate that if it's a generic type, it must be a list (not tuple, etc.) 45 | if origin is not None and origin is not list: 46 | msg = f"Only list[BaseModel] is supported for sequence types, got {pydantic_model}" 47 | raise TypeError(msg) 48 | 49 | self._type_adapter = TypeAdapter[T](pydantic_model) 50 | self._default_collection = default_collection 51 | self._raise_on_validation_error = raise_on_validation_error 52 | 53 | def _get_model_type_name(self) -> str: 54 | """Return the model type name for error messages.""" 55 | return "Pydantic model" 56 | -------------------------------------------------------------------------------- /docs/api/wrappers.md: -------------------------------------------------------------------------------- 1 | # Wrappers API Reference 2 | 3 | Complete API reference for all available wrappers. 4 | 5 | ## Base Wrapper 6 | 7 | ::: key_value.aio.wrappers.base.BaseWrapper 8 | options: 9 | show_source: false 10 | members: true 11 | 12 | ## CompressionWrapper 13 | 14 | ::: key_value.aio.wrappers.compression.CompressionWrapper 15 | options: 16 | show_source: false 17 | members: true 18 | 19 | ## FernetEncryptionWrapper 20 | 21 | ::: key_value.aio.wrappers.encryption.fernet.FernetEncryptionWrapper 22 | options: 23 | show_source: false 24 | members: true 25 | 26 | ## FallbackWrapper 27 | 28 | ::: key_value.aio.wrappers.fallback.FallbackWrapper 29 | options: 30 | show_source: false 31 | members: true 32 | 33 | ## LimitSizeWrapper 34 | 35 | ::: key_value.aio.wrappers.limit_size.LimitSizeWrapper 36 | options: 37 | show_source: false 38 | members: true 39 | 40 | ## LoggingWrapper 41 | 42 | ::: key_value.aio.wrappers.logging.LoggingWrapper 43 | options: 44 | show_source: false 45 | members: true 46 | 47 | ## PassthroughCacheWrapper 48 | 49 | ::: key_value.aio.wrappers.passthrough_cache.PassthroughCacheWrapper 50 | options: 51 | show_source: false 52 | members: true 53 | 54 | ## PrefixCollectionsWrapper 55 | 56 | ::: key_value.aio.wrappers.prefix_collections.PrefixCollectionsWrapper 57 | options: 58 | show_source: false 59 | members: true 60 | 61 | ## PrefixKeysWrapper 62 | 63 | ::: key_value.aio.wrappers.prefix_keys.PrefixKeysWrapper 64 | options: 65 | show_source: false 66 | members: true 67 | 68 | ## ReadOnlyWrapper 69 | 70 | ::: key_value.aio.wrappers.read_only.ReadOnlyWrapper 71 | options: 72 | show_source: false 73 | members: true 74 | 75 | ## RetryWrapper 76 | 77 | ::: key_value.aio.wrappers.retry.RetryWrapper 78 | options: 79 | show_source: false 80 | members: true 81 | 82 | ## SingleCollectionWrapper 83 | 84 | ::: key_value.aio.wrappers.single_collection.SingleCollectionWrapper 85 | options: 86 | show_source: false 87 | members: true 88 | 89 | ## TTLClampWrapper 90 | 91 | ::: key_value.aio.wrappers.ttl_clamp.TTLClampWrapper 92 | options: 93 | show_source: false 94 | members: true 95 | 96 | ## StatisticsWrapper 97 | 98 | ::: key_value.aio.wrappers.statistics.StatisticsWrapper 99 | options: 100 | show_source: false 101 | members: true 102 | 103 | ## TimeoutWrapper 104 | 105 | ::: key_value.aio.wrappers.timeout.TimeoutWrapper 106 | options: 107 | show_source: false 108 | members: true 109 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/ttl_clamp/wrapper.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'wrapper.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from collections.abc import Mapping, Sequence 5 | from typing import Any, SupportsFloat, overload 6 | 7 | from key_value.shared.utils.time_to_live import prepare_ttl 8 | from typing_extensions import override 9 | 10 | from key_value.sync.code_gen.protocols.key_value import KeyValue 11 | from key_value.sync.code_gen.wrappers.base import BaseWrapper 12 | 13 | 14 | class TTLClampWrapper(BaseWrapper): 15 | """Wrapper that enforces a maximum TTL for puts into the store. 16 | 17 | This wrapper only modifies write operations (put, put_many). All read operations 18 | (get, get_many, ttl, ttl_many, delete, delete_many) pass through unchanged to 19 | the underlying store. 20 | """ 21 | 22 | def __init__( 23 | self, key_value: KeyValue, min_ttl: SupportsFloat, max_ttl: SupportsFloat, missing_ttl: SupportsFloat | None = None 24 | ) -> None: 25 | """Initialize the TTL clamp wrapper. 26 | 27 | Args: 28 | key_value: The store to wrap. 29 | min_ttl: The minimum TTL for puts into the store. 30 | max_ttl: The maximum TTL for puts into the store. 31 | missing_ttl: The TTL to use for entries that do not have a TTL. Defaults to None. 32 | """ 33 | self.key_value: KeyValue = key_value 34 | self.min_ttl: float = float(min_ttl) 35 | self.max_ttl: float = float(max_ttl) 36 | self.missing_ttl: float | None = float(missing_ttl) if missing_ttl is not None else None 37 | 38 | super().__init__() 39 | 40 | @overload 41 | def _ttl_clamp(self, ttl: SupportsFloat) -> float: ... 42 | 43 | @overload 44 | def _ttl_clamp(self, ttl: SupportsFloat | None) -> float | None: ... 45 | 46 | def _ttl_clamp(self, ttl: SupportsFloat | None) -> float | None: 47 | if ttl is None: 48 | return self.missing_ttl 49 | 50 | ttl = prepare_ttl(t=ttl) 51 | 52 | return max(self.min_ttl, min(ttl, self.max_ttl)) 53 | 54 | @override 55 | def put(self, key: str, value: Mapping[str, Any], *, collection: str | None = None, ttl: SupportsFloat | None = None) -> None: 56 | self.key_value.put(collection=collection, key=key, value=value, ttl=self._ttl_clamp(ttl=ttl)) 57 | 58 | @override 59 | def put_many( 60 | self, keys: Sequence[str], values: Sequence[Mapping[str, Any]], *, collection: str | None = None, ttl: SupportsFloat | None = None 61 | ) -> None: 62 | self.key_value.put_many(keys=keys, values=values, collection=collection, ttl=self._ttl_clamp(ttl=ttl)) 63 | -------------------------------------------------------------------------------- /key-value/key-value-aio/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "py-key-value-aio" 3 | version = "0.3.0" 4 | description = "Async Key-Value" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | classifiers = [ 8 | "Development Status :: 3 - Alpha", 9 | "Intended Audience :: Developers", 10 | "License :: OSI Approved :: Apache Software License", 11 | "Programming Language :: Python :: 3", 12 | "Programming Language :: Python :: 3.10", 13 | "Programming Language :: Python :: 3.11", 14 | "Programming Language :: Python :: 3.12", 15 | "Programming Language :: Python :: 3.13", 16 | ] 17 | dependencies = [ 18 | "py-key-value-shared==0.3.0", 19 | "beartype>=0.20.0", 20 | ] 21 | 22 | 23 | [build-system] 24 | requires = ["uv_build>=0.8.2,<0.9.0"] 25 | build-backend = "uv_build" 26 | 27 | [tool.uv.build-backend] 28 | module-name = "key_value.aio" 29 | 30 | [tool.uv.sources] 31 | py-key-value-shared = { workspace = true } 32 | py-key-value-shared-test = { workspace = true } 33 | 34 | [project.optional-dependencies] 35 | memory = ["cachetools>=5.0.0"] 36 | disk = ["diskcache>=5.0.0", "pathvalidate>=3.3.1",] 37 | filetree = ["aiofile>=3.5.0", "anyio>=4.4.0"] 38 | redis = ["redis>=4.3.0"] 39 | mongodb = ["pymongo>=4.0.0"] 40 | valkey = ["valkey-glide>=2.1.0"] 41 | vault = ["hvac>=2.3.0", "types-hvac>=2.3.0"] 42 | memcached = ["aiomcache>=0.8.0"] 43 | elasticsearch = ["elasticsearch>=8.0.0", "aiohttp>=3.12"] 44 | dynamodb = ["aioboto3>=13.3.0", "types-aiobotocore-dynamodb>=2.16.0"] 45 | keyring = ["keyring>=25.6.0"] 46 | keyring-linux = ["keyring>=25.6.0", "dbus-python>=1.4.0"] 47 | pydantic = ["pydantic>=2.11.9"] 48 | rocksdb = [ 49 | "rocksdict>=0.3.24 ; python_version >= '3.12'", # RocksDB 0.3.24 is the first version to support Python 3.13 50 | "rocksdict>=0.3.2 ; python_version < '3.12'" 51 | ] 52 | duckdb = ["duckdb>=1.1.1", "pytz>=2025.2"] 53 | wrappers-encryption = ["cryptography>=45.0.0"] 54 | 55 | [tool.pytest.ini_options] 56 | asyncio_mode = "auto" 57 | addopts = [ 58 | "--inline-snapshot=disable", 59 | "-n=auto", 60 | "--dist=loadfile", 61 | "--maxfail=5" 62 | ] 63 | markers = [ 64 | "skip_on_ci: Skip running the test when running on CI", 65 | ] 66 | timeout = 10 67 | timeout_func_only = true 68 | 69 | env_files = [".env"] 70 | 71 | [dependency-groups] 72 | dev = [ 73 | "py-key-value-aio[memory,disk,filetree,redis,elasticsearch,memcached,mongodb,vault,dynamodb,rocksdb,duckdb]", 74 | "py-key-value-aio[valkey]; platform_system != 'Windows'", 75 | "py-key-value-aio[keyring]", 76 | "py-key-value-aio[pydantic]", 77 | "py-key-value-aio[wrappers-encryption]", 78 | "py-key-value[dev]", 79 | ] 80 | 81 | [tool.ruff] 82 | extend="../../pyproject.toml" 83 | 84 | [tool.pyright] 85 | extends = "../../pyproject.toml" 86 | -------------------------------------------------------------------------------- /key-value/key-value-sync/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "py-key-value-sync" 3 | version = "0.3.0" 4 | description = "Sync Key-Value" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | classifiers = [ 8 | "Development Status :: 3 - Alpha", 9 | "Intended Audience :: Developers", 10 | "License :: OSI Approved :: Apache Software License", 11 | "Programming Language :: Python :: 3", 12 | "Programming Language :: Python :: 3.10", 13 | "Programming Language :: Python :: 3.11", 14 | "Programming Language :: Python :: 3.12", 15 | "Programming Language :: Python :: 3.13", 16 | ] 17 | dependencies = [ 18 | "py-key-value-shared==0.3.0", 19 | "beartype>=0.20.0", 20 | ] 21 | 22 | [tool.uv.sources] 23 | py-key-value-shared = { workspace = true } 24 | py-key-value = { workspace = true } 25 | py-key-value-shared-test = { workspace = true } 26 | 27 | [build-system] 28 | requires = ["uv_build>=0.8.2,<0.9.0"] 29 | build-backend = "uv_build" 30 | 31 | [tool.uv.build-backend] 32 | module-name = "key_value.sync" 33 | 34 | [project.optional-dependencies] 35 | memory = ["cachetools>=5.0.0"] 36 | disk = ["diskcache>=5.0.0", "pathvalidate>=3.3.1",] 37 | filetree = ["anyio>=4.4.0"] 38 | redis = ["redis>=4.3.0"] 39 | mongodb = ["pymongo>=4.0.0"] 40 | valkey = ["valkey-glide-sync>=2.1.0"] 41 | vault = ["hvac>=2.3.0", "types-hvac>=2.3.0"] 42 | memcached = ["aiomcache>=0.8.0"] 43 | elasticsearch = ["elasticsearch>=8.0.0", "aiohttp>=3.12"] 44 | pydantic = ["pydantic>=2.11.9"] 45 | keyring = ["keyring>=25.6.0"] 46 | keyring-linux = ["keyring>=25.6.0", "dbus-python>=1.4.0"] 47 | rocksdb = [ 48 | "rocksdict>=0.3.24 ; python_version >= '3.12'", # RocksDB 0.3.24 is the first version to support Python 3.13 49 | "rocksdict>=0.3.2 ; python_version < '3.12'" 50 | ] 51 | duckdb = ["duckdb>=1.1.1", "pytz>=2025.2"] 52 | wrappers-encryption = ["cryptography>=45.0.0"] 53 | 54 | [tool.pytest.ini_options] 55 | asyncio_mode = "auto" 56 | addopts = [ 57 | "--inline-snapshot=disable", 58 | "-n=auto", 59 | "--dist=loadfile", 60 | "--maxfail=5" 61 | ] 62 | markers = [ 63 | "skip_on_ci: Skip running the test when running on CI", 64 | ] 65 | timeout = 10 66 | timeout_func_only = true 67 | 68 | env_files = [".env"] 69 | 70 | [dependency-groups] 71 | dev = [ 72 | "py-key-value-sync[memory,disk,filetree,redis,elasticsearch,memcached,mongodb,vault,rocksdb,duckdb]", 73 | "py-key-value-sync[valkey]; platform_system != 'Windows'", 74 | "py-key-value-sync[pydantic]", 75 | "py-key-value-sync[keyring]", 76 | "py-key-value-sync[wrappers-encryption]", 77 | "py-key-value[dev]", 78 | ] 79 | 80 | [tool.ruff] 81 | extend="../../pyproject.toml" 82 | 83 | [tool.pyright] 84 | extends = "../../pyproject.toml" 85 | 86 | exclude = [ 87 | "src/key_value/sync/code_gen/stores/redis/store.py" 88 | ] -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/windows_registry/test_windows_registry.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_windows_registry.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from typing import TYPE_CHECKING 5 | 6 | import pytest 7 | from typing_extensions import override 8 | 9 | from key_value.sync.code_gen.stores.base import BaseStore 10 | from tests.code_gen.conftest import detect_on_windows 11 | from tests.code_gen.stores.base import BaseStoreTests 12 | 13 | if TYPE_CHECKING: 14 | from key_value.sync.code_gen.stores.windows_registry.store import WindowsRegistryStore 15 | 16 | TEST_REGISTRY_PATH = "software\\py-key-value-test" 17 | 18 | 19 | @pytest.mark.skipif(condition=not detect_on_windows(), reason="WindowsRegistryStore is only available on Windows") 20 | @pytest.mark.filterwarnings("ignore:A configured store is unstable and may change in a backwards incompatible way. Use at your own risk.") 21 | class TestWindowsRegistryStore(BaseStoreTests): 22 | def cleanup(self): 23 | from winreg import HKEY_CURRENT_USER 24 | 25 | from key_value.sync.code_gen.stores.windows_registry.utils import delete_sub_keys 26 | 27 | delete_sub_keys(hive=HKEY_CURRENT_USER, sub_key=TEST_REGISTRY_PATH) 28 | 29 | @override 30 | @pytest.fixture 31 | def store(self) -> "WindowsRegistryStore": 32 | from key_value.sync.code_gen.stores.windows_registry.store import WindowsRegistryStore 33 | 34 | self.cleanup() 35 | 36 | return WindowsRegistryStore(registry_path=TEST_REGISTRY_PATH, hive="HKEY_CURRENT_USER") 37 | 38 | @pytest.fixture 39 | def sanitizing_store(self): 40 | from key_value.sync.code_gen.stores.windows_registry.store import ( 41 | WindowsRegistryStore, 42 | WindowsRegistryV1CollectionSanitizationStrategy, 43 | ) 44 | 45 | return WindowsRegistryStore( 46 | registry_path=TEST_REGISTRY_PATH, 47 | hive="HKEY_CURRENT_USER", 48 | collection_sanitization_strategy=WindowsRegistryV1CollectionSanitizationStrategy(), 49 | ) 50 | 51 | @override 52 | @pytest.mark.skip(reason="We do not test boundedness of registry stores") 53 | def test_not_unbounded(self, store: BaseStore): ... 54 | 55 | @override 56 | def test_long_collection_name(self, store: "WindowsRegistryStore", sanitizing_store: "WindowsRegistryStore"): # pyright: ignore[reportIncompatibleMethodOverride] 57 | with pytest.raises(Exception): # noqa: B017, PT011 58 | store.put(collection="test_collection" * 100, key="test_key", value={"test": "test"}) 59 | 60 | sanitizing_store.put(collection="test_collection" * 100, key="test_key", value={"test": "test"}) 61 | assert sanitizing_store.get(collection="test_collection" * 100, key="test_key") == {"test": "test"} 62 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/adapters/test_raise.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from key_value.shared.errors import MissingKeyError 3 | 4 | from key_value.aio.adapters.raise_on_missing import RaiseOnMissingAdapter 5 | from key_value.aio.stores.memory.store import MemoryStore 6 | 7 | 8 | @pytest.fixture 9 | async def store() -> MemoryStore: 10 | return MemoryStore() 11 | 12 | 13 | @pytest.fixture 14 | async def adapter(store: MemoryStore) -> RaiseOnMissingAdapter: 15 | return RaiseOnMissingAdapter(key_value=store) 16 | 17 | 18 | async def test_get(adapter: RaiseOnMissingAdapter): 19 | await adapter.put(collection="test", key="test", value={"test": "test"}) 20 | assert await adapter.get(collection="test", key="test") == {"test": "test"} 21 | 22 | 23 | async def test_get_missing(adapter: RaiseOnMissingAdapter): 24 | with pytest.raises(MissingKeyError): 25 | _ = await adapter.get(collection="test", key="test", raise_on_missing=True) 26 | 27 | 28 | async def test_get_many(adapter: RaiseOnMissingAdapter): 29 | await adapter.put(collection="test", key="test", value={"test": "test"}) 30 | await adapter.put(collection="test", key="test_2", value={"test": "test_2"}) 31 | assert await adapter.get_many(collection="test", keys=["test", "test_2"]) == [{"test": "test"}, {"test": "test_2"}] 32 | 33 | 34 | async def test_get_many_missing(adapter: RaiseOnMissingAdapter): 35 | await adapter.put(collection="test", key="test", value={"test": "test"}) 36 | with pytest.raises(MissingKeyError): 37 | _ = await adapter.get_many(collection="test", keys=["test", "test_2"], raise_on_missing=True) 38 | 39 | 40 | async def test_ttl(adapter: RaiseOnMissingAdapter): 41 | await adapter.put(collection="test", key="test", value={"test": "test"}, ttl=60) 42 | value, ttl = await adapter.ttl(collection="test", key="test") 43 | assert value == {"test": "test"} 44 | assert ttl is not None 45 | 46 | 47 | async def test_ttl_missing(adapter: RaiseOnMissingAdapter): 48 | with pytest.raises(MissingKeyError): 49 | _ = await adapter.ttl(collection="test", key="test", raise_on_missing=True) 50 | 51 | 52 | async def test_ttl_many(adapter: RaiseOnMissingAdapter): 53 | await adapter.put(collection="test", key="test", value={"test": "test"}, ttl=60) 54 | await adapter.put(collection="test", key="test_2", value={"test": "test_2"}, ttl=120) 55 | results = await adapter.ttl_many(collection="test", keys=["test", "test_2"]) 56 | assert results[0][0] == {"test": "test"} 57 | assert results[0][1] is not None 58 | assert results[1][0] == {"test": "test_2"} 59 | assert results[1][1] is not None 60 | 61 | 62 | async def test_ttl_many_missing(adapter: RaiseOnMissingAdapter): 63 | await adapter.put(collection="test", key="test", value={"test": "test"}, ttl=60) 64 | with pytest.raises(MissingKeyError): 65 | _ = await adapter.ttl_many(collection="test", keys=["test", "test_2"], raise_on_missing=True) 66 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/adapters/test_raise.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_raise.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from key_value.shared.errors import MissingKeyError 6 | 7 | from key_value.sync.code_gen.adapters.raise_on_missing import RaiseOnMissingAdapter 8 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 9 | 10 | 11 | @pytest.fixture 12 | def store() -> MemoryStore: 13 | return MemoryStore() 14 | 15 | 16 | @pytest.fixture 17 | def adapter(store: MemoryStore) -> RaiseOnMissingAdapter: 18 | return RaiseOnMissingAdapter(key_value=store) 19 | 20 | 21 | def test_get(adapter: RaiseOnMissingAdapter): 22 | adapter.put(collection="test", key="test", value={"test": "test"}) 23 | assert adapter.get(collection="test", key="test") == {"test": "test"} 24 | 25 | 26 | def test_get_missing(adapter: RaiseOnMissingAdapter): 27 | with pytest.raises(MissingKeyError): 28 | _ = adapter.get(collection="test", key="test", raise_on_missing=True) 29 | 30 | 31 | def test_get_many(adapter: RaiseOnMissingAdapter): 32 | adapter.put(collection="test", key="test", value={"test": "test"}) 33 | adapter.put(collection="test", key="test_2", value={"test": "test_2"}) 34 | assert adapter.get_many(collection="test", keys=["test", "test_2"]) == [{"test": "test"}, {"test": "test_2"}] 35 | 36 | 37 | def test_get_many_missing(adapter: RaiseOnMissingAdapter): 38 | adapter.put(collection="test", key="test", value={"test": "test"}) 39 | with pytest.raises(MissingKeyError): 40 | _ = adapter.get_many(collection="test", keys=["test", "test_2"], raise_on_missing=True) 41 | 42 | 43 | def test_ttl(adapter: RaiseOnMissingAdapter): 44 | adapter.put(collection="test", key="test", value={"test": "test"}, ttl=60) 45 | (value, ttl) = adapter.ttl(collection="test", key="test") 46 | assert value == {"test": "test"} 47 | assert ttl is not None 48 | 49 | 50 | def test_ttl_missing(adapter: RaiseOnMissingAdapter): 51 | with pytest.raises(MissingKeyError): 52 | _ = adapter.ttl(collection="test", key="test", raise_on_missing=True) 53 | 54 | 55 | def test_ttl_many(adapter: RaiseOnMissingAdapter): 56 | adapter.put(collection="test", key="test", value={"test": "test"}, ttl=60) 57 | adapter.put(collection="test", key="test_2", value={"test": "test_2"}, ttl=120) 58 | results = adapter.ttl_many(collection="test", keys=["test", "test_2"]) 59 | assert results[0][0] == {"test": "test"} 60 | assert results[0][1] is not None 61 | assert results[1][0] == {"test": "test_2"} 62 | assert results[1][1] is not None 63 | 64 | 65 | def test_ttl_many_missing(adapter: RaiseOnMissingAdapter): 66 | adapter.put(collection="test", key="test", value={"test": "test"}, ttl=60) 67 | with pytest.raises(MissingKeyError): 68 | _ = adapter.ttl_many(collection="test", keys=["test", "test_2"], raise_on_missing=True) 69 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # py-key-value Documentation 2 | 3 | Welcome to the **py-key-value** documentation! This library provides a pluggable 4 | interface for key-value stores with support for multiple backends, TTL handling, 5 | type safety, and extensible wrappers. 6 | 7 | ## Overview 8 | 9 | py-key-value is a Python framework that offers: 10 | 11 | - **Multiple backends**: DynamoDB, Elasticsearch, Memcached, MongoDB, Redis, 12 | RocksDB, Valkey, and In-memory, Disk, etc. 13 | - **TTL support**: Automatic expiration handling across all store types 14 | - **Type-safe**: Full type hints with Protocol-based interfaces 15 | - **Adapters**: Pydantic model support, raise-on-missing behavior, etc. 16 | - **Wrappers**: Statistics tracking, encryption, compression, and more 17 | - **Collection-based**: Organize keys into logical collections/namespaces 18 | - **Pluggable architecture**: Easy to add custom store implementations 19 | 20 | ## Quick Links 21 | 22 | - [Getting Started](getting-started.md) - Installation and basic usage 23 | - [Stores](stores.md) - Detailed documentation for all stores 24 | - [Wrappers](wrappers.md) - Detailed documentation for all wrappers 25 | - [Adapters](adapters.md) - Detailed documentation for all adapters 26 | - [API Reference](api/protocols.md) - Complete API documentation 27 | 28 | ## Installation 29 | 30 | Install the async library: 31 | 32 | ```bash 33 | pip install py-key-value-aio 34 | ``` 35 | 36 | Install with specific backend support: 37 | 38 | ```bash 39 | # Redis support 40 | pip install py-key-value-aio[redis] 41 | 42 | # DynamoDB support 43 | pip install py-key-value-aio[dynamodb] 44 | 45 | # All backends 46 | pip install py-key-value-aio[all] 47 | ``` 48 | 49 | ## Quick Example 50 | 51 | ```python 52 | from key_value.aio.stores.memory import MemoryStore 53 | 54 | # Create a store 55 | store = MemoryStore() 56 | 57 | # Store a value with TTL 58 | await store.put( 59 | key="user:123", 60 | value={"name": "Alice", "email": "alice@example.com"}, 61 | collection="users", 62 | ttl=3600 # 1 hour 63 | ) 64 | 65 | # Retrieve the value 66 | user = await store.get(key="user:123", collection="users") 67 | print(user) # {"name": "Alice", "email": "alice@example.com"} 68 | ``` 69 | 70 | ## For Framework Authors 71 | 72 | While key-value storage is valuable for individual projects, its true power 73 | emerges when framework authors use it as a **pluggable abstraction layer**. 74 | 75 | By coding your framework against the `AsyncKeyValue` protocol, you enable your 76 | users to choose their own storage backend without changing a single line of your 77 | framework code. 78 | 79 | [Learn more about using py-key-value in your framework](getting-started.md#for-framework-authors) 80 | 81 | ## Project Links 82 | 83 | - [GitHub Repository](https://github.com/strawgate/py-key-value) 84 | - [PyPI Package](https://pypi.org/project/py-key-value-aio/) 85 | - [Issue Tracker](https://github.com/strawgate/py-key-value/issues) 86 | 87 | ## License 88 | 89 | This project is licensed under the Apache License 2.0. 90 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "py-key-value" 3 | version = "0.3.0" 4 | description = "Key-Value Store Project" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | license = {text = "Apache-2.0"} 8 | 9 | 10 | [tool.uv.workspace] 11 | members = [ 12 | "key-value/key-value-aio", 13 | "key-value/key-value-sync", 14 | "key-value/key-value-shared", 15 | "key-value/key-value-shared-test", 16 | ] 17 | 18 | [tool.uv.sources] 19 | py-key-value-shared-test = { workspace = true } 20 | py-key-value = { workspace = true } 21 | 22 | [tool.ruff] 23 | target-version = "py310" 24 | lint.fixable = ["ALL"] 25 | lint.ignore = [ 26 | "COM812", 27 | "PLR0913", # Too many arguments, MCP Servers have a lot of arguments, OKAY?! 28 | ] 29 | lint.extend-select = [ 30 | "A", 31 | "ARG", 32 | "B", 33 | "C4", 34 | "COM", 35 | "DTZ", 36 | "E", 37 | "EM", 38 | "F", 39 | "FURB", 40 | "I", 41 | "LOG", 42 | "N", 43 | "PERF", 44 | "PIE", 45 | "PLR", 46 | "PLW", 47 | "PT", 48 | "PTH", 49 | "Q", 50 | "RET", 51 | "RSE", 52 | "RUF", 53 | "S", 54 | "SIM", 55 | "TC", 56 | "TID", 57 | "TRY", 58 | "UP", 59 | "W", 60 | ] 61 | 62 | line-length = 140 63 | 64 | [tool.ruff.lint.extend-per-file-ignores] 65 | "**/tests/*.py" = [ 66 | "S101", # Ignore asserts 67 | "DTZ005", # Ignore datetime.UTC 68 | "PLR2004", # Ignore magic values 69 | "E501", # Ignore line length 70 | "ARG001", # Unused argument, Pyright captures this for us 71 | "ARG002", # Unused argument, Pyright captures this for us 72 | ] 73 | "**/code_gen/**/*.py" = [ 74 | "ARG001", # Unused argument, Pyright captures this for us 75 | "ARG002", # Unused argument, Pyright captures this for us 76 | "E501", # Ignore long lines 77 | ] 78 | 79 | [project.optional-dependencies] 80 | dev = [ 81 | "ast-comments>=1.2.3", 82 | "py-key-value-shared-test", 83 | "basedpyright>=1.32.1", 84 | "dirty-equals>=0.10.0", 85 | "diskcache-stubs>=5.6.3.6.20240818", 86 | "docker>=7.1.0", 87 | "inline-snapshot>=0.30.1", 88 | "pytest-asyncio>=1.2.0", 89 | "pytest-dotenv>=0.5.2", 90 | "pytest-mock>=3.15.1", 91 | "pytest-timeout>=2.4.0", 92 | "pytest-xdist>=3.8.0", 93 | "pytest>=8.4.2", 94 | "ruff>=0.14.2" 95 | ] 96 | docs = [ 97 | "mkdocs>=1.6.0", 98 | "mkdocs-material>=9.5.0", 99 | "mkdocstrings[python]>=0.30.0", 100 | "mkdocstrings-python>=1.10.0" 101 | ] 102 | 103 | [tool.pyright] 104 | pythonVersion = "3.10" 105 | typeCheckingMode = "strict" 106 | reportExplicitAny = false 107 | reportMissingTypeStubs = false 108 | reportPrivateUsage = false 109 | #reportUnnecessaryTypeIgnoreComment = "error" 110 | include = [ 111 | "key-value" 112 | ] 113 | exclude = [ 114 | "**/playground/**", 115 | "**/node_modules/**", 116 | "**/examples/**", 117 | "**/references/**", 118 | "**/docs/**", 119 | "**/.venv/**", 120 | ] 121 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/stores/windows_registry/utils.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import winreg 3 | 4 | from key_value.shared.errors.store import StoreSetupError 5 | 6 | HiveType = int 7 | 8 | 9 | def get_reg_sz_value(hive: HiveType, sub_key: str, value_name: str) -> str | None: 10 | try: 11 | with winreg.OpenKey(key=hive, sub_key=sub_key) as reg_key: 12 | string, _ = winreg.QueryValueEx(reg_key, value_name) 13 | return string 14 | except (FileNotFoundError, OSError): 15 | return None 16 | 17 | 18 | def set_reg_sz_value(hive: HiveType, sub_key: str, value_name: str, value: str) -> None: 19 | try: 20 | with winreg.OpenKey(key=hive, sub_key=sub_key, access=winreg.KEY_WRITE) as reg_key: 21 | winreg.SetValueEx(reg_key, value_name, 0, winreg.REG_SZ, value) 22 | except FileNotFoundError as e: 23 | msg = f"Registry key '{sub_key}' does not exist" 24 | raise StoreSetupError(msg) from e 25 | except OSError as e: 26 | msg = f"Failed to set registry value '{value_name}' at '{sub_key}'" 27 | raise StoreSetupError(msg) from e 28 | 29 | 30 | def delete_reg_sz_value(hive: HiveType, sub_key: str, value_name: str) -> bool: 31 | try: 32 | with winreg.OpenKey(key=hive, sub_key=sub_key, access=winreg.KEY_WRITE) as reg_key: 33 | winreg.DeleteValue(reg_key, value_name) 34 | return True 35 | except (FileNotFoundError, OSError): 36 | return False 37 | 38 | 39 | def has_key(hive: HiveType, sub_key: str) -> bool: 40 | try: 41 | with winreg.OpenKey(key=hive, sub_key=sub_key): 42 | return True 43 | except (FileNotFoundError, OSError): 44 | return False 45 | 46 | 47 | def create_key(hive: HiveType, sub_key: str) -> None: 48 | try: 49 | key = winreg.CreateKey(hive, sub_key) 50 | key.Close() 51 | except OSError as e: 52 | msg = f"Failed to create registry key '{sub_key}'" 53 | raise StoreSetupError(msg) from e 54 | 55 | 56 | def delete_key(hive: HiveType, sub_key: str) -> bool: 57 | try: 58 | winreg.DeleteKey(hive, sub_key) 59 | except (FileNotFoundError, OSError): 60 | return False 61 | else: 62 | return True 63 | 64 | 65 | def delete_sub_keys(hive: HiveType, sub_key: str) -> None: 66 | try: 67 | with winreg.OpenKey(key=hive, sub_key=sub_key, access=winreg.KEY_WRITE | winreg.KEY_ENUMERATE_SUB_KEYS) as reg_key: 68 | while True: 69 | try: 70 | # Always enumerate at index 0 since keys shift after deletion 71 | next_child_key = winreg.EnumKey(reg_key, 0) 72 | except OSError: 73 | # No more subkeys 74 | break 75 | 76 | # Key already deleted or can't be deleted, skip it 77 | with contextlib.suppress(FileNotFoundError, OSError): 78 | winreg.DeleteKey(reg_key, next_child_key) 79 | except (FileNotFoundError, OSError): 80 | return 81 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/adapters/dataclass/adapter.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from dataclasses import is_dataclass 3 | from typing import Any, TypeVar, get_args, get_origin 4 | 5 | from key_value.shared.type_checking.bear_spray import bear_spray 6 | from pydantic.type_adapter import TypeAdapter 7 | 8 | from key_value.aio.adapters.pydantic.base import BasePydanticAdapter 9 | from key_value.aio.protocols.key_value import AsyncKeyValue 10 | 11 | T = TypeVar("T") 12 | 13 | 14 | class DataclassAdapter(BasePydanticAdapter[T]): 15 | """Adapter around a KVStore-compliant Store that allows type-safe persistence of dataclasses. 16 | 17 | This adapter works with both standard library dataclasses and Pydantic dataclasses, 18 | leveraging Pydantic's TypeAdapter for robust validation and serialization. 19 | """ 20 | 21 | _inner_type: type[Any] 22 | 23 | # Beartype cannot handle the parameterized type annotation (type[T]) used here for this generic dataclass adapter. 24 | # Using @bear_spray to bypass beartype's runtime checks for this specific method. 25 | @bear_spray 26 | def __init__( 27 | self, 28 | key_value: AsyncKeyValue, 29 | dataclass_type: type[T], 30 | default_collection: str | None = None, 31 | raise_on_validation_error: bool = False, 32 | ) -> None: 33 | """Create a new DataclassAdapter. 34 | 35 | Args: 36 | key_value: The AsyncKeyValue to use. 37 | dataclass_type: The dataclass type to use. Can be a single dataclass or list[dataclass]. 38 | default_collection: The default collection to use. 39 | raise_on_validation_error: Whether to raise a DeserializationError if validation fails during reads. Otherwise, 40 | calls will return None if validation fails. 41 | 42 | Raises: 43 | TypeError: If dataclass_type is not a dataclass type. 44 | """ 45 | self._key_value = key_value 46 | 47 | origin = get_origin(dataclass_type) 48 | self._is_list_model = origin is not None and isinstance(origin, type) and issubclass(origin, Sequence) 49 | 50 | # Extract the inner type for list models 51 | if self._is_list_model: 52 | args = get_args(dataclass_type) 53 | if not args: 54 | msg = f"List type {dataclass_type} must have a type argument" 55 | raise TypeError(msg) 56 | self._inner_type = args[0] 57 | else: 58 | self._inner_type = dataclass_type 59 | 60 | # Validate that the inner type is a dataclass 61 | if not is_dataclass(self._inner_type): 62 | msg = f"{self._inner_type} is not a dataclass" 63 | raise TypeError(msg) 64 | 65 | self._type_adapter = TypeAdapter[T](dataclass_type) 66 | self._default_collection = default_collection 67 | self._raise_on_validation_error = raise_on_validation_error 68 | 69 | def _get_model_type_name(self) -> str: 70 | """Return the model type name for error messages.""" 71 | return "dataclass" 72 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_timeout.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections.abc import Sequence 3 | 4 | import pytest 5 | from typing_extensions import override 6 | 7 | from key_value.aio.stores.memory.store import MemoryStore 8 | from key_value.aio.wrappers.timeout import TimeoutWrapper 9 | from tests.stores.base import BaseStoreTests 10 | 11 | 12 | class SlowStore(MemoryStore): 13 | """A store that takes a long time to respond.""" 14 | 15 | def __init__(self, delay: float = 1.0): 16 | super().__init__() 17 | self.delay = delay 18 | 19 | async def get(self, key: str, *, collection: str | None = None): 20 | await asyncio.sleep(self.delay) 21 | return await super().get(key=key, collection=collection) 22 | 23 | async def get_many(self, keys: Sequence[str], *, collection: str | None = None): 24 | await asyncio.sleep(self.delay) 25 | return await super().get_many(keys=keys, collection=collection) 26 | 27 | async def ttl(self, key: str, *, collection: str | None = None): 28 | await asyncio.sleep(self.delay) 29 | return await super().ttl(key=key, collection=collection) 30 | 31 | async def ttl_many(self, keys: Sequence[str], *, collection: str | None = None): 32 | await asyncio.sleep(self.delay) 33 | return await super().ttl_many(keys=keys, collection=collection) 34 | 35 | 36 | class TestTimeoutWrapper(BaseStoreTests): 37 | @override 38 | @pytest.fixture 39 | async def store(self, memory_store: MemoryStore) -> TimeoutWrapper: 40 | return TimeoutWrapper(key_value=memory_store, timeout=5.0) 41 | 42 | async def test_timeout_on_slow_operation(self): 43 | slow_store = SlowStore(delay=1.0) 44 | timeout_store = TimeoutWrapper(key_value=slow_store, timeout=0.1) 45 | 46 | # Should timeout 47 | with pytest.raises(asyncio.TimeoutError): 48 | await timeout_store.get(collection="test", key="test") 49 | 50 | async def test_no_timeout_on_fast_operation(self, memory_store: MemoryStore): 51 | timeout_store = TimeoutWrapper(key_value=memory_store, timeout=1.0) 52 | 53 | # Should succeed 54 | await timeout_store.put(collection="test", key="test", value={"test": "value"}) 55 | result = await timeout_store.get(collection="test", key="test") 56 | assert result == {"test": "value"} 57 | 58 | async def test_timeout_applies_to_all_operations(self): 59 | slow_store = SlowStore(delay=2.0) 60 | timeout_store = TimeoutWrapper(key_value=slow_store, timeout=0.1) 61 | 62 | # All operations should timeout 63 | with pytest.raises(asyncio.TimeoutError): 64 | await timeout_store.get(collection="test", key="test") 65 | 66 | with pytest.raises(asyncio.TimeoutError): 67 | await timeout_store.get_many(collection="test", keys=["test"]) 68 | 69 | with pytest.raises(asyncio.TimeoutError): 70 | await timeout_store.ttl(collection="test", key="test") 71 | 72 | with pytest.raises(asyncio.TimeoutError): 73 | await timeout_store.ttl_many(collection="test", keys=["test"]) 74 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/code_gen/run.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Awaitable, Callable, Coroutine 2 | from typing import Any, TypeVar 3 | 4 | from key_value.shared.code_gen.sleep import asleep, sleep 5 | 6 | 7 | async def await_awaitable(awaitable: Awaitable[Any]) -> Any: 8 | """ 9 | Equivalent to await awaitable, converted to await awaitable by async_to_sync. 10 | """ 11 | return await awaitable 12 | 13 | 14 | def run_function(function: Callable[..., Any]) -> Any: 15 | """ 16 | Equivalent to function(), converted to function() by async_to_sync. 17 | """ 18 | return function() 19 | 20 | 21 | def _calculate_delay(initial_delay: float, max_delay: float, exponential_base: float, attempt: int) -> float: 22 | """Calculate the delay for a given attempt using exponential backoff.""" 23 | delay = initial_delay * (exponential_base**attempt) 24 | return min(delay, max_delay) 25 | 26 | 27 | T = TypeVar("T") 28 | 29 | 30 | async def async_retry_operation( 31 | max_retries: int, 32 | retry_on: tuple[type[Exception], ...], 33 | initial_delay: float, 34 | max_delay: float, 35 | exponential_base: float, 36 | operation: Callable[..., Coroutine[Any, Any, T]], 37 | *args: Any, 38 | **kwargs: Any, 39 | ) -> T: 40 | """Execute an operation with retry logic.""" 41 | last_exception: Exception | None = None 42 | 43 | for attempt in range(max_retries + 1): 44 | try: 45 | return await operation(*args, **kwargs) 46 | except retry_on as e: # noqa: PERF203 47 | last_exception = e 48 | if attempt < max_retries: 49 | delay = _calculate_delay(initial_delay, max_delay, exponential_base, attempt) 50 | await asleep(delay) 51 | else: 52 | # Last attempt failed, re-raise 53 | raise 54 | 55 | # This should never be reached, but satisfy type checker 56 | if last_exception: 57 | raise last_exception 58 | msg = "Retry operation failed without exception" 59 | raise RuntimeError(msg) 60 | 61 | 62 | def retry_operation( 63 | max_retries: int, 64 | retry_on: tuple[type[Exception], ...], 65 | initial_delay: float, 66 | max_delay: float, 67 | exponential_base: float, 68 | operation: Callable[..., T], 69 | *args: Any, 70 | **kwargs: Any, 71 | ) -> T: 72 | """Execute an operation with retry logic.""" 73 | last_exception: Exception | None = None 74 | 75 | for attempt in range(max_retries + 1): 76 | try: 77 | return operation(*args, **kwargs) 78 | except retry_on as e: # noqa: PERF203 79 | last_exception = e 80 | if attempt < max_retries: 81 | delay = _calculate_delay(initial_delay, max_delay, exponential_base, attempt) 82 | sleep(delay) 83 | else: 84 | # Last attempt failed, re-raise 85 | raise 86 | 87 | # This should never be reached, but satisfy type checker 88 | if last_exception: 89 | raise last_exception 90 | msg = "Retry operation failed without exception" 91 | raise RuntimeError(msg) 92 | -------------------------------------------------------------------------------- /key-value/key-value-shared/src/key_value/shared/utils/time_to_live.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timedelta, timezone 3 | from typing import Any, SupportsFloat, overload 4 | 5 | from key_value.shared.errors import InvalidTTLError 6 | from key_value.shared.type_checking.bear_spray import bear_enforce 7 | 8 | 9 | def epoch_to_datetime(epoch: float) -> datetime: 10 | """Convert an epoch timestamp to a datetime object.""" 11 | return datetime.fromtimestamp(epoch, tz=timezone.utc) 12 | 13 | 14 | def now_as_epoch() -> float: 15 | """Get the current time as epoch seconds.""" 16 | return time.time() 17 | 18 | 19 | def now() -> datetime: 20 | """Get the current time as a datetime object.""" 21 | return datetime.now(tz=timezone.utc) 22 | 23 | 24 | def seconds_to(datetime: datetime) -> float: 25 | """Get the number of seconds between the current time and a datetime object.""" 26 | return (datetime - now()).total_seconds() 27 | 28 | 29 | def now_plus(seconds: float) -> datetime: 30 | """Get the current time plus a number of seconds as a datetime object.""" 31 | return datetime.now(tz=timezone.utc) + timedelta(seconds=seconds) 32 | 33 | 34 | def try_parse_datetime_str(value: Any) -> datetime | None: # pyright: ignore[reportAny] 35 | try: 36 | if isinstance(value, str): 37 | return datetime.fromisoformat(value) 38 | except ValueError: 39 | return None 40 | 41 | return None 42 | 43 | 44 | @overload 45 | def prepare_ttl(t: SupportsFloat) -> float: ... 46 | 47 | 48 | @overload 49 | def prepare_ttl(t: SupportsFloat | None) -> float | None: ... 50 | 51 | 52 | def prepare_ttl(t: SupportsFloat | None) -> float | None: 53 | """Prepare a TTL for use in a put operation. 54 | 55 | If a TTL is provided, it will be validated and returned as a float. 56 | If a None is provided, None will be returned. 57 | 58 | If the provided TTL is not a float or float-adjacent type, an InvalidTTLError will be raised. In addition, 59 | if a bool is provided, an InvalidTTLError will be raised. If the user passes TTL=True, true becomes `1` and the 60 | entry immediately expires which is likely not what the user intended. 61 | """ 62 | try: 63 | return _validate_ttl(t=t) 64 | except TypeError as e: 65 | raise InvalidTTLError(ttl=t, extra_info={"type": type(t).__name__}) from e 66 | 67 | 68 | @bear_enforce 69 | def _validate_ttl(t: SupportsFloat | None) -> float | None: 70 | if t is None: 71 | return None 72 | 73 | if isinstance(t, bool): 74 | raise InvalidTTLError(ttl=t, extra_info={"type": type(t).__name__}) 75 | 76 | ttl = float(t) 77 | 78 | if ttl <= 0: 79 | raise InvalidTTLError(ttl=t) 80 | 81 | return ttl 82 | 83 | 84 | def prepare_entry_timestamps(ttl: SupportsFloat | None) -> tuple[datetime, float | None, datetime | None]: 85 | created_at: datetime = now() 86 | 87 | ttl_seconds: float | None = prepare_ttl(t=ttl) 88 | 89 | expires_at: datetime | None = None 90 | if ttl_seconds is not None: 91 | expires_at = created_at + timedelta(seconds=ttl_seconds) 92 | 93 | return created_at, ttl_seconds, expires_at 94 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_read_only.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from key_value.shared.errors.wrappers.read_only import ReadOnlyError 3 | from typing_extensions import override 4 | 5 | from key_value.aio.stores.memory.store import MemoryStore 6 | from key_value.aio.wrappers.read_only import ReadOnlyWrapper 7 | 8 | 9 | class TestReadOnlyWrapper: 10 | @pytest.fixture 11 | async def memory_store(self) -> MemoryStore: 12 | return MemoryStore() 13 | 14 | @override 15 | @pytest.fixture 16 | async def store(self, memory_store: MemoryStore) -> ReadOnlyWrapper: 17 | # Pre-populate the store with test data 18 | await memory_store.put(collection="test", key="test", value={"test": "test"}) 19 | return ReadOnlyWrapper(key_value=memory_store, raise_on_write=False) 20 | 21 | async def test_read_operations_allowed(self, memory_store: MemoryStore): 22 | # Pre-populate store 23 | await memory_store.put(collection="test", key="test", value={"test": "value"}) 24 | 25 | read_only_store = ReadOnlyWrapper(key_value=memory_store, raise_on_write=True) 26 | 27 | # Read operations should work 28 | result = await read_only_store.get(collection="test", key="test") 29 | assert result == {"test": "value"} 30 | 31 | results = await read_only_store.get_many(collection="test", keys=["test"]) 32 | assert results == [{"test": "value"}] 33 | 34 | value, _ = await read_only_store.ttl(collection="test", key="test") 35 | assert value == {"test": "value"} 36 | 37 | async def test_write_operations_raise_error(self, memory_store: MemoryStore): 38 | read_only_store = ReadOnlyWrapper(key_value=memory_store, raise_on_write=True) 39 | 40 | # Write operations should raise ReadOnlyError 41 | with pytest.raises(ReadOnlyError): 42 | await read_only_store.put(collection="test", key="test", value={"test": "value"}) 43 | 44 | with pytest.raises(ReadOnlyError): 45 | await read_only_store.put_many(collection="test", keys=["test"], values=[{"test": "value"}]) 46 | 47 | with pytest.raises(ReadOnlyError): 48 | await read_only_store.delete(collection="test", key="test") 49 | 50 | with pytest.raises(ReadOnlyError): 51 | await read_only_store.delete_many(collection="test", keys=["test"]) 52 | 53 | async def test_write_operations_silent_ignore(self, memory_store: MemoryStore): 54 | read_only_store = ReadOnlyWrapper(key_value=memory_store, raise_on_write=False) 55 | 56 | # Write operations should be silently ignored 57 | await read_only_store.put(collection="test", key="new_key", value={"test": "value"}) 58 | 59 | # Verify nothing was written 60 | result = await memory_store.get(collection="test", key="new_key") 61 | assert result is None 62 | 63 | # Delete should return False 64 | deleted = await read_only_store.delete(collection="test", key="test") 65 | assert deleted is False 66 | 67 | # Delete many should return 0 68 | deleted_count = await read_only_store.delete_many(collection="test", keys=["test"]) 69 | assert deleted_count == 0 70 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/stores/windows_registry/utils.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'utils.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import contextlib 5 | import winreg 6 | 7 | from key_value.shared.errors.store import StoreSetupError 8 | 9 | HiveType = int 10 | 11 | 12 | def get_reg_sz_value(hive: HiveType, sub_key: str, value_name: str) -> str | None: 13 | try: 14 | with winreg.OpenKey(key=hive, sub_key=sub_key) as reg_key: 15 | (string, _) = winreg.QueryValueEx(reg_key, value_name) 16 | return string 17 | except (FileNotFoundError, OSError): 18 | return None 19 | 20 | 21 | def set_reg_sz_value(hive: HiveType, sub_key: str, value_name: str, value: str) -> None: 22 | try: 23 | with winreg.OpenKey(key=hive, sub_key=sub_key, access=winreg.KEY_WRITE) as reg_key: 24 | winreg.SetValueEx(reg_key, value_name, 0, winreg.REG_SZ, value) 25 | except FileNotFoundError as e: 26 | msg = f"Registry key '{sub_key}' does not exist" 27 | raise StoreSetupError(msg) from e 28 | except OSError as e: 29 | msg = f"Failed to set registry value '{value_name}' at '{sub_key}'" 30 | raise StoreSetupError(msg) from e 31 | 32 | 33 | def delete_reg_sz_value(hive: HiveType, sub_key: str, value_name: str) -> bool: 34 | try: 35 | with winreg.OpenKey(key=hive, sub_key=sub_key, access=winreg.KEY_WRITE) as reg_key: 36 | winreg.DeleteValue(reg_key, value_name) 37 | return True 38 | except (FileNotFoundError, OSError): 39 | return False 40 | 41 | 42 | def has_key(hive: HiveType, sub_key: str) -> bool: 43 | try: 44 | with winreg.OpenKey(key=hive, sub_key=sub_key): 45 | return True 46 | except (FileNotFoundError, OSError): 47 | return False 48 | 49 | 50 | def create_key(hive: HiveType, sub_key: str) -> None: 51 | try: 52 | key = winreg.CreateKey(hive, sub_key) 53 | key.Close() 54 | except OSError as e: 55 | msg = f"Failed to create registry key '{sub_key}'" 56 | raise StoreSetupError(msg) from e 57 | 58 | 59 | def delete_key(hive: HiveType, sub_key: str) -> bool: 60 | try: 61 | winreg.DeleteKey(hive, sub_key) 62 | except (FileNotFoundError, OSError): 63 | return False 64 | else: 65 | return True 66 | 67 | 68 | def delete_sub_keys(hive: HiveType, sub_key: str) -> None: 69 | try: 70 | with winreg.OpenKey(key=hive, sub_key=sub_key, access=winreg.KEY_WRITE | winreg.KEY_ENUMERATE_SUB_KEYS) as reg_key: 71 | while True: 72 | try: 73 | # Always enumerate at index 0 since keys shift after deletion 74 | next_child_key = winreg.EnumKey(reg_key, 0) 75 | except OSError: 76 | # No more subkeys 77 | break 78 | 79 | # Key already deleted or can't be deleted, skip it 80 | with contextlib.suppress(FileNotFoundError, OSError): 81 | winreg.DeleteKey(reg_key, next_child_key) 82 | except (FileNotFoundError, OSError): 83 | return 84 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/adapters/dataclass/adapter.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'adapter.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from collections.abc import Sequence 5 | from dataclasses import is_dataclass 6 | from typing import Any, TypeVar, get_args, get_origin 7 | 8 | from key_value.shared.type_checking.bear_spray import bear_spray 9 | from pydantic.type_adapter import TypeAdapter 10 | 11 | from key_value.sync.code_gen.adapters.pydantic.base import BasePydanticAdapter 12 | from key_value.sync.code_gen.protocols.key_value import KeyValue 13 | 14 | T = TypeVar("T") 15 | 16 | 17 | class DataclassAdapter(BasePydanticAdapter[T]): 18 | """Adapter around a KVStore-compliant Store that allows type-safe persistence of dataclasses. 19 | 20 | This adapter works with both standard library dataclasses and Pydantic dataclasses, 21 | leveraging Pydantic's TypeAdapter for robust validation and serialization. 22 | """ 23 | 24 | _inner_type: type[Any] 25 | 26 | # Beartype cannot handle the parameterized type annotation (type[T]) used here for this generic dataclass adapter. 27 | # Using @bear_spray to bypass beartype's runtime checks for this specific method. 28 | 29 | @bear_spray 30 | def __init__( 31 | self, key_value: KeyValue, dataclass_type: type[T], default_collection: str | None = None, raise_on_validation_error: bool = False 32 | ) -> None: 33 | """Create a new DataclassAdapter. 34 | 35 | Args: 36 | key_value: The KeyValue to use. 37 | dataclass_type: The dataclass type to use. Can be a single dataclass or list[dataclass]. 38 | default_collection: The default collection to use. 39 | raise_on_validation_error: Whether to raise a DeserializationError if validation fails during reads. Otherwise, 40 | calls will return None if validation fails. 41 | 42 | Raises: 43 | TypeError: If dataclass_type is not a dataclass type. 44 | """ 45 | self._key_value = key_value 46 | 47 | origin = get_origin(dataclass_type) 48 | self._is_list_model = origin is not None and isinstance(origin, type) and issubclass(origin, Sequence) 49 | 50 | # Extract the inner type for list models 51 | if self._is_list_model: 52 | args = get_args(dataclass_type) 53 | if not args: 54 | msg = f"List type {dataclass_type} must have a type argument" 55 | raise TypeError(msg) 56 | self._inner_type = args[0] 57 | else: 58 | self._inner_type = dataclass_type 59 | 60 | # Validate that the inner type is a dataclass 61 | if not is_dataclass(self._inner_type): 62 | msg = f"{self._inner_type} is not a dataclass" 63 | raise TypeError(msg) 64 | 65 | self._type_adapter = TypeAdapter[T](dataclass_type) 66 | self._default_collection = default_collection 67 | self._raise_on_validation_error = raise_on_validation_error 68 | 69 | def _get_model_type_name(self) -> str: 70 | """Return the model type name for error messages.""" 71 | return "dataclass" 72 | -------------------------------------------------------------------------------- /key-value/key-value-sync/tests/code_gen/stores/wrappers/test_read_only.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'test_read_only.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | import pytest 5 | from key_value.shared.errors.wrappers.read_only import ReadOnlyError 6 | from typing_extensions import override 7 | 8 | from key_value.sync.code_gen.stores.memory.store import MemoryStore 9 | from key_value.sync.code_gen.wrappers.read_only import ReadOnlyWrapper 10 | 11 | 12 | class TestReadOnlyWrapper: 13 | @pytest.fixture 14 | def memory_store(self) -> MemoryStore: 15 | return MemoryStore() 16 | 17 | @override 18 | @pytest.fixture 19 | def store(self, memory_store: MemoryStore) -> ReadOnlyWrapper: 20 | # Pre-populate the store with test data 21 | memory_store.put(collection="test", key="test", value={"test": "test"}) 22 | return ReadOnlyWrapper(key_value=memory_store, raise_on_write=False) 23 | 24 | def test_read_operations_allowed(self, memory_store: MemoryStore): 25 | # Pre-populate store 26 | memory_store.put(collection="test", key="test", value={"test": "value"}) 27 | 28 | read_only_store = ReadOnlyWrapper(key_value=memory_store, raise_on_write=True) 29 | 30 | # Read operations should work 31 | result = read_only_store.get(collection="test", key="test") 32 | assert result == {"test": "value"} 33 | 34 | results = read_only_store.get_many(collection="test", keys=["test"]) 35 | assert results == [{"test": "value"}] 36 | 37 | (value, _) = read_only_store.ttl(collection="test", key="test") 38 | assert value == {"test": "value"} 39 | 40 | def test_write_operations_raise_error(self, memory_store: MemoryStore): 41 | read_only_store = ReadOnlyWrapper(key_value=memory_store, raise_on_write=True) 42 | 43 | # Write operations should raise ReadOnlyError 44 | with pytest.raises(ReadOnlyError): 45 | read_only_store.put(collection="test", key="test", value={"test": "value"}) 46 | 47 | with pytest.raises(ReadOnlyError): 48 | read_only_store.put_many(collection="test", keys=["test"], values=[{"test": "value"}]) 49 | 50 | with pytest.raises(ReadOnlyError): 51 | read_only_store.delete(collection="test", key="test") 52 | 53 | with pytest.raises(ReadOnlyError): 54 | read_only_store.delete_many(collection="test", keys=["test"]) 55 | 56 | def test_write_operations_silent_ignore(self, memory_store: MemoryStore): 57 | read_only_store = ReadOnlyWrapper(key_value=memory_store, raise_on_write=False) 58 | 59 | # Write operations should be silently ignored 60 | read_only_store.put(collection="test", key="new_key", value={"test": "value"}) 61 | 62 | # Verify nothing was written 63 | result = memory_store.get(collection="test", key="new_key") 64 | assert result is None 65 | 66 | # Delete should return False 67 | deleted = read_only_store.delete(collection="test", key="test") 68 | assert deleted is False 69 | 70 | # Delete many should return 0 71 | deleted_count = read_only_store.delete_many(collection="test", keys=["test"]) 72 | assert deleted_count == 0 73 | -------------------------------------------------------------------------------- /key-value/key-value-aio/tests/stores/wrappers/test_retry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing_extensions import override 3 | 4 | from key_value.aio.stores.memory.store import MemoryStore 5 | from key_value.aio.wrappers.retry import RetryWrapper 6 | from tests.stores.base import BaseStoreTests 7 | 8 | 9 | class FailingStore(MemoryStore): 10 | """A store that fails a certain number of times before succeeding.""" 11 | 12 | def __init__(self, failures_before_success: int = 2): 13 | super().__init__() 14 | self.failures_before_success = failures_before_success 15 | self.attempt_count = 0 16 | 17 | async def get(self, key: str, *, collection: str | None = None): 18 | self.attempt_count += 1 19 | if self.attempt_count <= self.failures_before_success: 20 | msg = "Simulated connection error" 21 | raise ConnectionError(msg) 22 | return await super().get(key=key, collection=collection) 23 | 24 | def reset_attempts(self): 25 | self.attempt_count = 0 26 | 27 | 28 | class TestRetryWrapper(BaseStoreTests): 29 | @override 30 | @pytest.fixture 31 | async def store(self, memory_store: MemoryStore) -> RetryWrapper: 32 | return RetryWrapper(key_value=memory_store, max_retries=3, initial_delay=0.01) 33 | 34 | async def test_retry_succeeds_after_failures(self): 35 | failing_store = FailingStore(failures_before_success=2) 36 | retry_store = RetryWrapper(key_value=failing_store, max_retries=3, initial_delay=0.01) 37 | 38 | # Store a value first 39 | await retry_store.put(collection="test", key="test", value={"test": "value"}) 40 | failing_store.reset_attempts() 41 | 42 | # Should succeed after 2 failures 43 | result = await retry_store.get(collection="test", key="test") 44 | assert result == {"test": "value"} 45 | assert failing_store.attempt_count == 3 # 2 failures + 1 success 46 | 47 | async def test_retry_fails_after_max_retries(self): 48 | failing_store = FailingStore(failures_before_success=10) # More failures than max_retries 49 | retry_store = RetryWrapper(key_value=failing_store, max_retries=2, initial_delay=0.01) 50 | 51 | # Should fail after exhausting retries 52 | with pytest.raises(ConnectionError): 53 | await retry_store.get(collection="test", key="test") 54 | 55 | assert failing_store.attempt_count == 3 # Initial attempt + 2 retries 56 | 57 | async def test_retry_with_different_exception(self): 58 | failing_store = FailingStore(failures_before_success=1) 59 | # Only retry on TimeoutError, not ConnectionError 60 | retry_store = RetryWrapper(key_value=failing_store, max_retries=3, initial_delay=0.01, retry_on=(TimeoutError,)) 61 | 62 | # Should fail immediately without retries 63 | with pytest.raises(ConnectionError): 64 | await retry_store.get(collection="test", key="test") 65 | 66 | assert failing_store.attempt_count == 1 # No retries 67 | 68 | async def test_retry_no_failures(self, memory_store: MemoryStore): 69 | retry_store = RetryWrapper(key_value=memory_store, max_retries=3, initial_delay=0.01) 70 | 71 | # Normal operation should work without retries 72 | await retry_store.put(collection="test", key="test", value={"test": "value"}) 73 | result = await retry_store.get(collection="test", key="test") 74 | assert result == {"test": "value"} 75 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/timeout/wrapper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections.abc import Mapping, Sequence 3 | from typing import Any, SupportsFloat 4 | 5 | from typing_extensions import override 6 | 7 | from key_value.aio.protocols.key_value import AsyncKeyValue 8 | from key_value.aio.wrappers.base import BaseWrapper 9 | 10 | 11 | class TimeoutWrapper(BaseWrapper): 12 | """Wrapper that adds timeout limits to all operations. 13 | 14 | This wrapper ensures that no operation takes longer than the specified timeout. 15 | If an operation exceeds the timeout, it raises asyncio.TimeoutError. This is useful 16 | for preventing operations from hanging indefinitely and for enforcing SLAs. 17 | """ 18 | 19 | def __init__( 20 | self, 21 | key_value: AsyncKeyValue, 22 | timeout: float = 5.0, 23 | ) -> None: 24 | """Initialize the timeout wrapper. 25 | 26 | Args: 27 | key_value: The store to wrap. 28 | timeout: Timeout in seconds for all operations. Defaults to 5.0 seconds. 29 | """ 30 | self.key_value: AsyncKeyValue = key_value 31 | self.timeout: float = timeout 32 | 33 | super().__init__() 34 | 35 | @override 36 | async def get(self, key: str, *, collection: str | None = None) -> dict[str, Any] | None: 37 | return await asyncio.wait_for(self.key_value.get(key=key, collection=collection), timeout=self.timeout) 38 | 39 | @override 40 | async def get_many(self, keys: Sequence[str], *, collection: str | None = None) -> list[dict[str, Any] | None]: 41 | return await asyncio.wait_for(self.key_value.get_many(keys=keys, collection=collection), timeout=self.timeout) 42 | 43 | @override 44 | async def ttl(self, key: str, *, collection: str | None = None) -> tuple[dict[str, Any] | None, float | None]: 45 | return await asyncio.wait_for(self.key_value.ttl(key=key, collection=collection), timeout=self.timeout) 46 | 47 | @override 48 | async def ttl_many(self, keys: Sequence[str], *, collection: str | None = None) -> list[tuple[dict[str, Any] | None, float | None]]: 49 | return await asyncio.wait_for(self.key_value.ttl_many(keys=keys, collection=collection), timeout=self.timeout) 50 | 51 | @override 52 | async def put(self, key: str, value: Mapping[str, Any], *, collection: str | None = None, ttl: SupportsFloat | None = None) -> None: 53 | return await asyncio.wait_for(self.key_value.put(key=key, value=value, collection=collection, ttl=ttl), timeout=self.timeout) 54 | 55 | @override 56 | async def put_many( 57 | self, 58 | keys: Sequence[str], 59 | values: Sequence[Mapping[str, Any]], 60 | *, 61 | collection: str | None = None, 62 | ttl: SupportsFloat | None = None, 63 | ) -> None: 64 | return await asyncio.wait_for( 65 | self.key_value.put_many(keys=keys, values=values, collection=collection, ttl=ttl), timeout=self.timeout 66 | ) 67 | 68 | @override 69 | async def delete(self, key: str, *, collection: str | None = None) -> bool: 70 | return await asyncio.wait_for(self.key_value.delete(key=key, collection=collection), timeout=self.timeout) 71 | 72 | @override 73 | async def delete_many(self, keys: Sequence[str], *, collection: str | None = None) -> int: 74 | return await asyncio.wait_for(self.key_value.delete_many(keys=keys, collection=collection), timeout=self.timeout) 75 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | This monorepo contains two Python packages: 4 | 5 | - `py-key-value-aio` (async; supported) 6 | - `py-key-value-sync` (sync; generated from async) 7 | 8 | ## Prerequisites 9 | 10 | ### Option 1: DevContainer (Recommended) 11 | 12 | - Docker Desktop or compatible container runtime 13 | - Visual Studio Code with the Dev Containers extension 14 | - Open the repository in VSCode and select "Reopen in Container" when prompted 15 | 16 | ### Option 2: Local Development 17 | 18 | - Python 3.10 (the sync codegen targets 3.10) 19 | - `uv` for dependency management and running tools 20 | - Node.js and npm for markdown linting 21 | 22 | ## Setup 23 | 24 | ```bash 25 | make sync 26 | ``` 27 | 28 | ## Common Commands 29 | 30 | Run `make help` to see all available targets. The Makefile supports both 31 | whole-repo and per-project operations. 32 | 33 | ### Lint and Format 34 | 35 | ```bash 36 | # Lint everything (Python + Markdown) 37 | make lint 38 | 39 | # Lint a specific project 40 | make lint PROJECT=key-value/key-value-aio 41 | ``` 42 | 43 | ### Type Checking 44 | 45 | ```bash 46 | # Type check everything 47 | make typecheck 48 | 49 | # Type check a specific project 50 | make typecheck PROJECT=key-value/key-value-aio 51 | ``` 52 | 53 | ### Testing 54 | 55 | ```bash 56 | # Run all tests 57 | make test 58 | 59 | # Run tests for a specific project 60 | make test PROJECT=key-value/key-value-aio 61 | 62 | # Convenience targets for specific packages 63 | make test-aio # async package 64 | make test-sync # sync package 65 | make test-shared # shared package 66 | ``` 67 | 68 | ### Building 69 | 70 | ```bash 71 | # Build all packages 72 | make build 73 | 74 | # Build a specific project 75 | make build PROJECT=key-value/key-value-aio 76 | ``` 77 | 78 | ## Generate/Update Sync Package 79 | 80 | The sync package is generated from the async package. After changes to the 81 | async code, regenerate the sync package: 82 | 83 | ```bash 84 | make codegen 85 | ``` 86 | 87 | Notes: 88 | 89 | - The codegen script lints the generated code automatically. 90 | - Some extras differ between async and sync (e.g., valkey). Refer to each 91 | package's README for current extras. 92 | 93 | ## Pre-commit Checks 94 | 95 | Before committing, run: 96 | 97 | ```bash 98 | make precommit 99 | ``` 100 | 101 | This runs linting, type checking, and code generation. 102 | 103 | **Important**: CI will fail if `make codegen lint` has not been run before 104 | committing. The `codegen_check` job in the test workflow verifies that 105 | running these commands produces no file changes, ensuring all generated 106 | code and formatting is up to date. 107 | 108 | ## Using Makefile in CI 109 | 110 | The Makefile targets support per-project operations, making them 111 | suitable for CI workflows: 112 | 113 | ```yaml 114 | # Example: CI workflow step 115 | - name: "Lint" 116 | run: make lint PROJECT=${{ matrix.project }} 117 | 118 | - name: "Type Check" 119 | run: make typecheck PROJECT=${{ matrix.project }} 120 | 121 | - name: "Test" 122 | run: make test PROJECT=${{ matrix.project }} 123 | ``` 124 | 125 | ## Project Layout 126 | 127 | - Async package: `key-value/key-value-aio/` 128 | - Sync package: `key-value/key-value-sync/` 129 | - Shared utilities: `key-value/key-value-shared/` 130 | - Codegen script: `scripts/build_sync_library.py` 131 | - Makefile: Root directory 132 | 133 | ## Releasing 134 | 135 | TBD 136 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/base.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping, Sequence 2 | from typing import Any, SupportsFloat 3 | 4 | from key_value.shared.type_checking.bear_spray import bear_enforce 5 | from typing_extensions import override 6 | 7 | from key_value.aio.protocols.key_value import AsyncKeyValue 8 | 9 | 10 | class BaseWrapper(AsyncKeyValue): 11 | """A base wrapper for KVStore implementations that passes through to the underlying store. 12 | 13 | This class implements the passthrough pattern where all operations are delegated to the wrapped 14 | key-value store without modification. It serves as a foundation for creating custom wrappers that 15 | need to intercept, modify, or enhance specific operations while passing through others unchanged. 16 | 17 | To create a custom wrapper, subclass this class and override only the methods you need to customize. 18 | All other operations will automatically pass through to the underlying store. 19 | 20 | Example: 21 | class LoggingWrapper(BaseWrapper): 22 | async def get(self, key: str, *, collection: str | None = None): 23 | logger.info(f"Getting key: {key}") 24 | return await super().get(key, collection=collection) 25 | 26 | Attributes: 27 | key_value: The underlying AsyncKeyValue store that operations are delegated to. 28 | """ 29 | 30 | key_value: AsyncKeyValue 31 | 32 | @bear_enforce 33 | @override 34 | async def get(self, key: str, *, collection: str | None = None) -> dict[str, Any] | None: 35 | return await self.key_value.get(collection=collection, key=key) 36 | 37 | @bear_enforce 38 | @override 39 | async def get_many(self, keys: Sequence[str], *, collection: str | None = None) -> list[dict[str, Any] | None]: 40 | return await self.key_value.get_many(collection=collection, keys=keys) 41 | 42 | @bear_enforce 43 | @override 44 | async def ttl(self, key: str, *, collection: str | None = None) -> tuple[dict[str, Any] | None, float | None]: 45 | return await self.key_value.ttl(collection=collection, key=key) 46 | 47 | @bear_enforce 48 | @override 49 | async def ttl_many(self, keys: Sequence[str], *, collection: str | None = None) -> list[tuple[dict[str, Any] | None, float | None]]: 50 | return await self.key_value.ttl_many(collection=collection, keys=keys) 51 | 52 | @bear_enforce 53 | @override 54 | async def put(self, key: str, value: Mapping[str, Any], *, collection: str | None = None, ttl: SupportsFloat | None = None) -> None: 55 | return await self.key_value.put(collection=collection, key=key, value=value, ttl=ttl) 56 | 57 | @bear_enforce 58 | @override 59 | async def put_many( 60 | self, 61 | keys: Sequence[str], 62 | values: Sequence[Mapping[str, Any]], 63 | *, 64 | collection: str | None = None, 65 | ttl: SupportsFloat | None = None, 66 | ) -> None: 67 | return await self.key_value.put_many(keys=keys, values=values, collection=collection, ttl=ttl) 68 | 69 | @bear_enforce 70 | @override 71 | async def delete(self, key: str, *, collection: str | None = None) -> bool: 72 | return await self.key_value.delete(collection=collection, key=key) 73 | 74 | @bear_enforce 75 | @override 76 | async def delete_many(self, keys: Sequence[str], *, collection: str | None = None) -> int: 77 | return await self.key_value.delete_many(keys=keys, collection=collection) 78 | -------------------------------------------------------------------------------- /key-value/key-value-aio/src/key_value/aio/wrappers/default_value/wrapper.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping, Sequence 2 | from typing import Any, SupportsFloat 3 | 4 | from key_value.shared.utils.managed_entry import dump_to_json, load_from_json 5 | from typing_extensions import override 6 | 7 | from key_value.aio.protocols.key_value import AsyncKeyValue 8 | from key_value.aio.wrappers.base import BaseWrapper 9 | 10 | 11 | class DefaultValueWrapper(BaseWrapper): 12 | """A wrapper that returns a default value when a key is not found. 13 | 14 | This wrapper provides dict.get(key, default) behavior for the key-value store, 15 | allowing you to specify a default value to return instead of None when a key doesn't exist. 16 | 17 | It does not store the default value in the underlying key-value store and the TTL returned with the default 18 | value is hard-coded based on the default_ttl parameter. Picking a default_ttl requires careful consideration 19 | of how the value will be used and if any other wrappers will be used that may rely on the TTL. 20 | """ 21 | 22 | key_value: AsyncKeyValue # Alias for BaseWrapper compatibility 23 | _default_ttl: float | None 24 | _default_value_json: str 25 | 26 | def __init__( 27 | self, 28 | key_value: AsyncKeyValue, 29 | default_value: Mapping[str, Any], 30 | default_ttl: SupportsFloat | None = None, 31 | ) -> None: 32 | """Initialize the DefaultValueWrapper. 33 | 34 | Args: 35 | key_value: The underlying key-value store to wrap. 36 | default_value: The default value to return when a key is not found. 37 | default_ttl: The TTL to return to the caller for default values. Defaults to None. 38 | """ 39 | self.key_value = key_value 40 | self._default_value_json = dump_to_json(obj=dict(default_value)) 41 | self._default_ttl = None if default_ttl is None else float(default_ttl) 42 | 43 | super().__init__() 44 | 45 | def _new_default_value(self) -> dict[str, Any]: 46 | return load_from_json(json_str=self._default_value_json) 47 | 48 | @override 49 | async def get(self, key: str, *, collection: str | None = None) -> dict[str, Any] | None: 50 | result = await self.key_value.get(key=key, collection=collection) 51 | return result if result is not None else self._new_default_value() 52 | 53 | @override 54 | async def get_many(self, keys: Sequence[str], *, collection: str | None = None) -> list[dict[str, Any] | None]: 55 | results = await self.key_value.get_many(keys=keys, collection=collection) 56 | return [result if result is not None else self._new_default_value() for result in results] 57 | 58 | @override 59 | async def ttl(self, key: str, *, collection: str | None = None) -> tuple[dict[str, Any] | None, float | None]: 60 | result, ttl_value = await self.key_value.ttl(key=key, collection=collection) 61 | if result is None: 62 | return (self._new_default_value(), self._default_ttl) 63 | return (result, ttl_value) 64 | 65 | @override 66 | async def ttl_many(self, keys: Sequence[str], *, collection: str | None = None) -> list[tuple[dict[str, Any] | None, float | None]]: 67 | results = await self.key_value.ttl_many(keys=keys, collection=collection) 68 | return [ 69 | (result, ttl_value) if result is not None else (self._new_default_value(), self._default_ttl) for result, ttl_value in results 70 | ] 71 | -------------------------------------------------------------------------------- /key-value/key-value-sync/src/key_value/sync/code_gen/wrappers/base.py: -------------------------------------------------------------------------------- 1 | # WARNING: this file is auto-generated by 'build_sync_library.py' 2 | # from the original file 'base.py' 3 | # DO NOT CHANGE! Change the original file instead. 4 | from collections.abc import Mapping, Sequence 5 | from typing import Any, SupportsFloat 6 | 7 | from key_value.shared.type_checking.bear_spray import bear_enforce 8 | from typing_extensions import override 9 | 10 | from key_value.sync.code_gen.protocols.key_value import KeyValue 11 | 12 | 13 | class BaseWrapper(KeyValue): 14 | """A base wrapper for KVStore implementations that passes through to the underlying store. 15 | 16 | This class implements the passthrough pattern where all operations are delegated to the wrapped 17 | key-value store without modification. It serves as a foundation for creating custom wrappers that 18 | need to intercept, modify, or enhance specific operations while passing through others unchanged. 19 | 20 | To create a custom wrapper, subclass this class and override only the methods you need to customize. 21 | All other operations will automatically pass through to the underlying store. 22 | 23 | Example: 24 | class LoggingWrapper(BaseWrapper): 25 | async def get(self, key: str, *, collection: str | None = None): 26 | logger.info(f"Getting key: {key}") 27 | return await super().get(key, collection=collection) 28 | 29 | Attributes: 30 | key_value: The underlying KeyValue store that operations are delegated to. 31 | """ 32 | 33 | key_value: KeyValue 34 | 35 | @bear_enforce 36 | @override 37 | def get(self, key: str, *, collection: str | None = None) -> dict[str, Any] | None: 38 | return self.key_value.get(collection=collection, key=key) 39 | 40 | @bear_enforce 41 | @override 42 | def get_many(self, keys: Sequence[str], *, collection: str | None = None) -> list[dict[str, Any] | None]: 43 | return self.key_value.get_many(collection=collection, keys=keys) 44 | 45 | @bear_enforce 46 | @override 47 | def ttl(self, key: str, *, collection: str | None = None) -> tuple[dict[str, Any] | None, float | None]: 48 | return self.key_value.ttl(collection=collection, key=key) 49 | 50 | @bear_enforce 51 | @override 52 | def ttl_many(self, keys: Sequence[str], *, collection: str | None = None) -> list[tuple[dict[str, Any] | None, float | None]]: 53 | return self.key_value.ttl_many(collection=collection, keys=keys) 54 | 55 | @bear_enforce 56 | @override 57 | def put(self, key: str, value: Mapping[str, Any], *, collection: str | None = None, ttl: SupportsFloat | None = None) -> None: 58 | return self.key_value.put(collection=collection, key=key, value=value, ttl=ttl) 59 | 60 | @bear_enforce 61 | @override 62 | def put_many( 63 | self, keys: Sequence[str], values: Sequence[Mapping[str, Any]], *, collection: str | None = None, ttl: SupportsFloat | None = None 64 | ) -> None: 65 | return self.key_value.put_many(keys=keys, values=values, collection=collection, ttl=ttl) 66 | 67 | @bear_enforce 68 | @override 69 | def delete(self, key: str, *, collection: str | None = None) -> bool: 70 | return self.key_value.delete(collection=collection, key=key) 71 | 72 | @bear_enforce 73 | @override 74 | def delete_many(self, keys: Sequence[str], *, collection: str | None = None) -> int: 75 | return self.key_value.delete_many(keys=keys, collection=collection) 76 | --------------------------------------------------------------------------------