Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-py-key-value-aio for
openSUSE:Factory checked in at 2026-09-21 12:24:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-py-key-value-aio (Old)
and /work/SRC/openSUSE:Factory/.python-py-key-value-aio.new.383539 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-py-key-value-aio"
Mon Sep 21 12:24:33 2026 rev:2 rq:1379422 version:0.4.6
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-py-key-value-aio/python-py-key-value-aio.changes
2026-06-19 17:39:28.027420432 +0200
+++
/work/SRC/openSUSE:Factory/.python-py-key-value-aio.new.383539/python-py-key-value-aio.changes
2026-09-21 12:24:38.742868693 +0200
@@ -1,0 +2,14 @@
+Mon Sep 21 08:34:02 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.4.6:
+ * Add FileTreeStore.cull() to reclaim the disk space of expired
+ entries, which are otherwise only removed when something
+ reads the key, and fix a get() race
+ * Pass the username through in RedisStore
+ * A key literally named "info" is no longer silently hidden
+- Require aiofile >= 3.12.3, following upstream, and require
+ typing_extensions: it is an unconditional dependency whose
+ imports are unguarded, and nothing pulled it in on the Python
+ flavours we build
+
+-------------------------------------------------------------------
Old:
----
py_key_value_aio-0.4.5.tar.gz
New:
----
py_key_value_aio-0.4.6.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-py-key-value-aio.spec ++++++
--- /var/tmp/diff_new_pack.MR4ftl/_old 2026-09-21 12:24:40.095925243 +0200
+++ /var/tmp/diff_new_pack.MR4ftl/_new 2026-09-21 12:24:40.098925368 +0200
@@ -17,28 +17,30 @@
Name: python-py-key-value-aio
-Version: 0.4.5
+Version: 0.4.6
Release: 0
Summary: Async key-value store abstraction with multiple backends
License: Apache-2.0
URL: https://github.com/strawgate/py-key-value
-Source:
https://files.pythonhosted.org/packages/source/p/py-key-value-aio/py_key_value_aio-%{version}.tar.gz
-BuildRequires: %{python_module aiofile >= 3.5.0}
+Source:
https://files.pythonhosted.org/packages/source/p/py_key_value_aio/py_key_value_aio-%{version}.tar.gz
+BuildRequires: %{python_module aiofile >= 3.12.3}
BuildRequires: %{python_module anyio >= 4.4.0}
BuildRequires: %{python_module beartype >= 0.20.0}
BuildRequires: %{python_module cachetools >= 5.0.0}
BuildRequires: %{python_module keyring >= 25.6.0}
BuildRequires: %{python_module pip}
+BuildRequires: %{python_module typing_extensions >= 4.15.0}
BuildRequires: %{python_module uv-build}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
-# uv-backend wheels don't expose deps to pythondistdeps, so require them by
hand
-Requires: python-aiofile >= 3.5.0
+# dist-info deps are not turned into RPM Requires, so list them by hand
+Requires: python-aiofile >= 3.12.3
Requires: python-anyio >= 4.4.0
Requires: python-beartype >= 0.20.0
Requires: python-cachetools >= 5.0.0
Requires: python-keyring >= 25.6.0
+Requires: python-typing_extensions >= 4.15.0
BuildArch: noarch
%python_subpackages
@@ -59,7 +61,7 @@
%python_expand %fdupes %{buildroot}%{$python_sitelib}
%check
-%python_expand PYTHONPATH=%{buildroot}%{$python_sitelib} $python -B -c "import
key_value.aio"
+%python_expand PYTHONPATH=%{buildroot}%{$python_sitelib} $python -B -c "import
key_value.aio.stores.memory"
%files %{python_files}
%doc README.md
++++++ py_key_value_aio-0.4.5.tar.gz -> py_key_value_aio-0.4.6.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/py_key_value_aio-0.4.5/PKG-INFO
new/py_key_value_aio-0.4.6/PKG-INFO
--- old/py_key_value_aio-0.4.5/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
+++ new/py_key_value_aio-0.4.6/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: py-key-value-aio
-Version: 0.4.5
+Version: 0.4.6
Summary: Async Key-Value Store - A pluggable interface for KV Stores
License-Expression: Apache-2.0
License-File: LICENSE
@@ -29,7 +29,8 @@
Requires-Dist: types-aiobotocore-dynamodb>=2.16.0 ; extra == 'dynamodb'
Requires-Dist: elasticsearch>=8.0.0 ; extra == 'elasticsearch'
Requires-Dist: aiohttp>=3.12 ; extra == 'elasticsearch'
-Requires-Dist: aiofile>=3.5.0 ; extra == 'filetree'
+Requires-Dist: aiofile>=3.5.0,!=3.9.0,<3.10 ; python_full_version < '3.11' and
extra == 'filetree'
+Requires-Dist: aiofile>=3.12.3 ; python_full_version >= '3.11' and extra ==
'filetree'
Requires-Dist: anyio>=4.4.0 ; extra == 'filetree'
Requires-Dist: google-cloud-firestore>=2.13.0 ; extra == 'firestore'
Requires-Dist: google-auth>=2.24.0 ; extra == 'firestore'
@@ -94,6 +95,11 @@
- [Adapters Guide](https://strawgate.com/py-key-value/adapters/)
- [API Reference](https://strawgate.com/py-key-value/api/protocols/)
+> **Troubleshooting a cache or database issue?** Instrument the client you use
+> with OpenTelemetry so you can inspect the store call alongside the request
and
+> service that triggered it. [Pydantic Logfire](https://pydantic.dev/logfire/)
+> can collect those traces, logs, and metrics in one place.
+
## Why use this library?
- **Multiple backends**: Aerospike, Azure Tables, chDB, DuckDB, DynamoDB,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/py_key_value_aio-0.4.5/README.md
new/py_key_value_aio-0.4.6/README.md
--- old/py_key_value_aio-0.4.5/README.md 1970-01-01 01:00:00.000000000
+0100
+++ new/py_key_value_aio-0.4.6/README.md 1970-01-01 01:00:00.000000000
+0100
@@ -14,6 +14,11 @@
- [Adapters Guide](https://strawgate.com/py-key-value/adapters/)
- [API Reference](https://strawgate.com/py-key-value/api/protocols/)
+> **Troubleshooting a cache or database issue?** Instrument the client you use
+> with OpenTelemetry so you can inspect the store call alongside the request
and
+> service that triggered it. [Pydantic Logfire](https://pydantic.dev/logfire/)
+> can collect those traces, logs, and metrics in one place.
+
## Why use this library?
- **Multiple backends**: Aerospike, Azure Tables, chDB, DuckDB, DynamoDB,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/py_key_value_aio-0.4.5/pyproject.toml
new/py_key_value_aio-0.4.6/pyproject.toml
--- old/py_key_value_aio-0.4.5/pyproject.toml 1970-01-01 01:00:00.000000000
+0100
+++ new/py_key_value_aio-0.4.6/pyproject.toml 1970-01-01 01:00:00.000000000
+0100
@@ -1,6 +1,6 @@
[project]
name = "py-key-value-aio"
-version = "0.4.5"
+version = "0.4.6"
description = "Async Key-Value Store - A pluggable interface for KV Stores"
readme = "README.md"
requires-python = ">=3.10"
@@ -20,71 +20,77 @@
"typing-extensions>=4.15.0",
]
-[build-system]
-requires = ["uv_build>=0.11.4,<0.12"]
-build-backend = "uv_build"
-
-[tool.uv.build-backend]
-module-name = "key_value"
-
[project.optional-dependencies]
memory = ["cachetools>=5.0.0"]
-disk = ["diskcache>=5.0.0", "pathvalidate>=3.3.1"]
-filetree = ["aiofile>=3.5.0", "anyio>=4.4.0"]
+disk = [
+ "diskcache>=5.0.0",
+ "pathvalidate>=3.3.1",
+]
+filetree = [
+ "aiofile>=3.5.0,!=3.9.0,<3.10 ; python_version < '3.11'",
+ "aiofile>=3.12.3 ; python_version >= '3.11'",
+ "anyio>=4.4.0",
+]
redis = ["redis>=4.3.0"]
mongodb = ["pymongo>=4.9.0"]
valkey = ["valkey-glide>=2.1.0"]
-vault = ["hvac>=2.3.0", "types-hvac>=2.3.0"]
+vault = [
+ "hvac>=2.3.0",
+ "types-hvac>=2.3.0",
+]
memcached = ["aiomcache>=0.8.0"]
-elasticsearch = ["elasticsearch>=8.0.0", "aiohttp>=3.12"]
+elasticsearch = [
+ "elasticsearch>=8.0.0",
+ "aiohttp>=3.12",
+]
opensearch = ["opensearch-py[async]>=2.0.0"]
-dynamodb = ["aioboto3>=13.3.0", "types-aiobotocore-dynamodb>=2.16.0"]
-s3 = ["aioboto3>=13.3.0", "types-aiobotocore-s3>=2.16.0"]
-azure-tables = ["azure-data-tables>=12.4.0", "azure-core>=1.29.0"]
+dynamodb = [
+ "aioboto3>=13.3.0",
+ "types-aiobotocore-dynamodb>=2.16.0",
+]
+s3 = [
+ "aioboto3>=13.3.0",
+ "types-aiobotocore-s3>=2.16.0",
+]
+azure-tables = [
+ "azure-data-tables>=12.4.0",
+ "azure-core>=1.29.0",
+]
keyring = ["keyring>=25.6.0"]
-keyring-linux = ["keyring>=25.6.0", "dbus-python>=1.4.0"]
+keyring-linux = [
+ "keyring>=25.6.0",
+ "dbus-python>=1.4.0",
+]
pydantic = ["pydantic>=2.11.9"]
aerospike = ["aerospike>=16.0.0"]
rocksdb = [
"rocksdict>=0.3.24 ; python_version >= '3.12'",
- "rocksdict>=0.3.2 ; python_version < '3.12'"
+ "rocksdict>=0.3.2 ; python_version < '3.12'",
+]
+duckdb = [
+ "duckdb>=1.1.1",
+ "pytz>=2025.2",
]
-duckdb = ["duckdb>=1.1.1", "pytz>=2025.2"]
chdb = ["chdb>=4.1.1"]
postgresql = ["asyncpg>=0.30.0"]
-firestore = ["google-cloud-firestore>=2.13.0", "google-auth>=2.24.0"]
+firestore = [
+ "google-cloud-firestore>=2.13.0",
+ "google-auth>=2.24.0",
+]
wrappers-encryption = ["cryptography>=45.0.0"]
docs = [
"mkdocs>=1.6.0",
"mkdocs-material>=9.5.0",
"mkdocstrings[python]>=0.30.0",
- "mkdocstrings-python>=1.10.0"
+ "mkdocstrings-python>=1.10.0",
]
-[dependency-groups]
-dev = [
-
"py-key-value-aio[memory,disk,filetree,redis,elasticsearch,opensearch,memcached,mongodb,vault,dynamodb,s3,azure-tables,rocksdb,duckdb,firestore,postgresql]",
- "py-key-value-aio[chdb]; platform_system != 'Windows'",
- "py-key-value-aio[valkey]; platform_system != 'Windows'",
- "py-key-value-aio[aerospike]; platform_system != 'Windows'",
- "py-key-value-aio[keyring]",
- "py-key-value-aio[pydantic]",
- "py-key-value-aio[wrappers-encryption]",
- "ast-comments>=1.2.3",
- "basedpyright>=1.32.1",
- "dirty-equals>=0.10.0",
- "diskcache-stubs>=5.6.3.6.20240818",
- "docker>=7.1.0",
- "inline-snapshot>=0.30.1",
-
"testcontainers[redis,mongodb,elasticsearch,memcached,vault,generic]>=4.10.0",
- "pytest-asyncio>=1.2.0",
- "pytest-dotenv>=0.5.2",
- "pytest-mock>=3.15.1",
- "pytest-timeout>=2.4.0",
- "pytest-xdist>=3.8.0",
- "pytest>=8.4.2",
- "ruff>=0.14.2"
-]
+[build-system]
+requires = ["uv_build>=0.11.4,<0.12"]
+build-backend = "uv_build"
+
+[tool.uv.build-backend]
+module-name = "key_value"
[tool.pytest.ini_options]
asyncio_mode = "auto"
@@ -92,11 +98,9 @@
"--inline-snapshot=disable",
"-n=auto",
"--dist=loadfile",
- "--maxfail=5"
-]
-markers = [
- "skip_on_ci: Skip running the test when running on CI",
+ "--maxfail=5",
]
+markers = ["skip_on_ci: Skip running the test when running on CI"]
filterwarnings = [
"ignore:The @wait_container_is_ready decorator is
deprecated.*:DeprecationWarning",
"ignore:enable_cleanup_closed ignored
because.*:DeprecationWarning:aiohttp.connector",
@@ -107,12 +111,15 @@
[tool.ruff]
target-version = "py310"
-lint.fixable = ["ALL"]
-lint.ignore = [
+line-length = 140
+
+[tool.ruff.lint]
+fixable = ["ALL"]
+ignore = [
"COM812",
"PLR0913",
]
-lint.extend-select = [
+extend-select = [
"A",
"ARG",
"B",
@@ -144,7 +151,6 @@
"UP",
"W",
]
-line-length = 140
[tool.ruff.lint.extend-per-file-ignores]
"**/tests/*.py" = [
@@ -167,7 +173,7 @@
reportUnnecessaryTypeIgnoreComment = true
include = [
"src",
- "tests"
+ "tests",
]
exclude = [
"**/playground/**",
@@ -177,3 +183,28 @@
"**/docs/**",
"**/.venv/**",
]
+
+[dependency-groups]
+dev = [
+
"py-key-value-aio[memory,disk,filetree,redis,elasticsearch,opensearch,memcached,mongodb,vault,dynamodb,s3,azure-tables,rocksdb,duckdb,firestore,postgresql]",
+ "py-key-value-aio[chdb]; platform_system != 'Windows'",
+ "py-key-value-aio[valkey]; platform_system != 'Windows'",
+ "py-key-value-aio[aerospike]; platform_system != 'Windows'",
+ "py-key-value-aio[keyring]",
+ "py-key-value-aio[pydantic]",
+ "py-key-value-aio[wrappers-encryption]",
+ "ast-comments>=1.2.3",
+ "basedpyright>=1.32.1",
+ "dirty-equals>=0.10.0",
+ "diskcache-stubs>=5.6.3.6.20240818",
+ "docker>=7.1.0",
+ "inline-snapshot>=0.30.1",
+
"testcontainers[redis,mongodb,elasticsearch,memcached,vault,generic]>=4.10.0",
+ "pytest-asyncio>=1.2.0",
+ "pytest-dotenv>=0.5.2",
+ "pytest-mock>=3.15.1",
+ "pytest-timeout>=2.4.0",
+ "pytest-xdist>=3.8.0",
+ "pytest>=8.4.2",
+ "ruff>=0.14.2",
+]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/py_key_value_aio-0.4.5/pyproject.toml.orig
new/py_key_value_aio-0.4.6/pyproject.toml.orig
--- old/py_key_value_aio-0.4.5/pyproject.toml.orig 1970-01-01
01:00:00.000000000 +0100
+++ new/py_key_value_aio-0.4.6/pyproject.toml.orig 1970-01-01
01:00:00.000000000 +0100
@@ -0,0 +1,188 @@
+[project]
+name = "py-key-value-aio"
+version = "0.4.6"
+description = "Async Key-Value Store - A pluggable interface for KV Stores"
+readme = "README.md"
+requires-python = ">=3.10"
+license = "Apache-2.0"
+license-files = ["LICENSE"]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+dependencies = [
+ "beartype>=0.20.0",
+ "typing-extensions>=4.15.0",
+]
+
+[build-system]
+requires = ["uv_build>=0.11.4,<0.12"]
+build-backend = "uv_build"
+
+[tool.uv.build-backend]
+module-name = "key_value"
+
+[project.optional-dependencies]
+memory = ["cachetools>=5.0.0"]
+disk = ["diskcache>=5.0.0", "pathvalidate>=3.3.1"]
+filetree = [
+ # aiofile 3.9.0 reads package metadata with unguarded dict access
(`metadata["Home-page"]`),
+ # which can raise KeyError depending on how the installed metadata was
built (see #350).
+ # Fixed upstream in 3.10.0+, but aiofile dropped Python 3.10 support in
that same release,
+ # so exclude the broken 3.9.0 and fall back to 3.8.8 (the next-newest
3.10-compatible release,
+ # which predates the broken metadata-reading code entirely) for Python
3.10.
+ "aiofile>=3.5.0,!=3.9.0,<3.10 ; python_version < '3.11'",
+ "aiofile>=3.12.3 ; python_version >= '3.11'",
+ "anyio>=4.4.0",
+]
+redis = ["redis>=4.3.0"]
+mongodb = ["pymongo>=4.9.0"]
+valkey = ["valkey-glide>=2.1.0"]
+vault = ["hvac>=2.3.0", "types-hvac>=2.3.0"]
+memcached = ["aiomcache>=0.8.0"]
+elasticsearch = ["elasticsearch>=8.0.0", "aiohttp>=3.12"]
+opensearch = ["opensearch-py[async]>=2.0.0"]
+dynamodb = ["aioboto3>=13.3.0", "types-aiobotocore-dynamodb>=2.16.0"]
+s3 = ["aioboto3>=13.3.0", "types-aiobotocore-s3>=2.16.0"]
+azure-tables = ["azure-data-tables>=12.4.0", "azure-core>=1.29.0"]
+keyring = ["keyring>=25.6.0"]
+keyring-linux = ["keyring>=25.6.0", "dbus-python>=1.4.0"]
+pydantic = ["pydantic>=2.11.9"]
+aerospike = ["aerospike>=16.0.0"]
+rocksdb = [
+ "rocksdict>=0.3.24 ; python_version >= '3.12'",
+ "rocksdict>=0.3.2 ; python_version < '3.12'"
+]
+duckdb = ["duckdb>=1.1.1", "pytz>=2025.2"]
+chdb = ["chdb>=4.1.1"]
+postgresql = ["asyncpg>=0.30.0"]
+firestore = ["google-cloud-firestore>=2.13.0", "google-auth>=2.24.0"]
+wrappers-encryption = ["cryptography>=45.0.0"]
+docs = [
+ "mkdocs>=1.6.0",
+ "mkdocs-material>=9.5.0",
+ "mkdocstrings[python]>=0.30.0",
+ "mkdocstrings-python>=1.10.0"
+]
+
+[dependency-groups]
+dev = [
+
"py-key-value-aio[memory,disk,filetree,redis,elasticsearch,opensearch,memcached,mongodb,vault,dynamodb,s3,azure-tables,rocksdb,duckdb,firestore,postgresql]",
+ "py-key-value-aio[chdb]; platform_system != 'Windows'",
+ "py-key-value-aio[valkey]; platform_system != 'Windows'",
+ "py-key-value-aio[aerospike]; platform_system != 'Windows'",
+ "py-key-value-aio[keyring]",
+ "py-key-value-aio[pydantic]",
+ "py-key-value-aio[wrappers-encryption]",
+ "ast-comments>=1.2.3",
+ "basedpyright>=1.32.1",
+ "dirty-equals>=0.10.0",
+ "diskcache-stubs>=5.6.3.6.20240818",
+ "docker>=7.1.0",
+ "inline-snapshot>=0.30.1",
+
"testcontainers[redis,mongodb,elasticsearch,memcached,vault,generic]>=4.10.0",
+ "pytest-asyncio>=1.2.0",
+ "pytest-dotenv>=0.5.2",
+ "pytest-mock>=3.15.1",
+ "pytest-timeout>=2.4.0",
+ "pytest-xdist>=3.8.0",
+ "pytest>=8.4.2",
+ "ruff>=0.14.2"
+]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+addopts = [
+ "--inline-snapshot=disable",
+ "-n=auto",
+ "--dist=loadfile",
+ "--maxfail=5"
+]
+markers = [
+ "skip_on_ci: Skip running the test when running on CI",
+]
+filterwarnings = [
+ "ignore:The @wait_container_is_ready decorator is
deprecated.*:DeprecationWarning",
+ "ignore:enable_cleanup_closed ignored
because.*:DeprecationWarning:aiohttp.connector",
+]
+timeout = 10
+timeout_func_only = true
+env_files = [".env"]
+
+[tool.ruff]
+target-version = "py310"
+lint.fixable = ["ALL"]
+lint.ignore = [
+ "COM812",
+ "PLR0913",
+]
+lint.extend-select = [
+ "A",
+ "ARG",
+ "B",
+ "C4",
+ "COM",
+ "DTZ",
+ "E",
+ "EM",
+ "F",
+ "FURB",
+ "I",
+ "LOG",
+ "N",
+ "PERF",
+ "PIE",
+ "PLR",
+ "PLW",
+ "PT",
+ "PTH",
+ "Q",
+ "RET",
+ "RSE",
+ "RUF",
+ "S",
+ "SIM",
+ "TC",
+ "TID",
+ "TRY",
+ "UP",
+ "W",
+]
+line-length = 140
+
+[tool.ruff.lint.extend-per-file-ignores]
+"**/tests/*.py" = [
+ "S101",
+ "S105",
+ "S106",
+ "DTZ005",
+ "PLR2004",
+ "E501",
+ "ARG001",
+ "ARG002",
+]
+
+[tool.pyright]
+pythonVersion = "3.10"
+typeCheckingMode = "strict"
+reportExplicitAny = false
+reportMissingTypeStubs = false
+reportPrivateUsage = false
+reportUnnecessaryTypeIgnoreComment = true
+include = [
+ "src",
+ "tests"
+]
+exclude = [
+ "**/playground/**",
+ "**/node_modules/**",
+ "**/examples/**",
+ "**/references/**",
+ "**/docs/**",
+ "**/.venv/**",
+]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/py_key_value_aio-0.4.5/src/key_value/aio/stores/filetree/store.py
new/py_key_value_aio-0.4.6/src/key_value/aio/stores/filetree/store.py
--- old/py_key_value_aio-0.4.5/src/key_value/aio/stores/filetree/store.py
1970-01-01 01:00:00.000000000 +0100
+++ new/py_key_value_aio-0.4.6/src/key_value/aio/stores/filetree/store.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,7 @@
"""FileTreeStore implementation using async filesystem operations."""
import contextlib
+import logging
import os
import tempfile
from collections.abc import AsyncGenerator
@@ -21,9 +22,11 @@
from key_value.aio._utils.time_to_live import now
from key_value.aio.errors import PathSecurityError
from key_value.aio.stores.base import (
- BaseStore,
+ BaseCullStore,
)
+logger = logging.getLogger(__name__)
+
DIRECTORY_ALLOWED_CHARACTERS = ALPHANUMERIC_CHARACTERS + "_"
MAX_FILE_NAME_LENGTH = 255
@@ -253,11 +256,7 @@
await validate_path_within_directory(path=path,
root_directory=self.root_directory)
async def _list_file_paths(self) -> AsyncGenerator[AsyncPath]:
- async for item_path in AsyncPath(self.directory).iterdir():
- if not await item_path.is_file() or item_path.suffix != ".json":
- continue
- if item_path.stem == "info":
- continue
+ async for item_path in iter_key_files(AsyncPath(self.directory)):
yield item_path
async def get_entry(self, *, key: str) -> ManagedEntry | None:
@@ -267,11 +266,11 @@
# Security validation
await self._validate_path_security(path=key_path)
- if not await key_path.exists():
+ try:
+ data_dict: dict[str, Any] = await read_file(file=key_path)
+ except FileNotFoundError:
return None
- data_dict: dict[str, Any] = await read_file(file=key_path)
-
return self.serialization_adapter.load_dict(data=data_dict)
async def put_entry(self, *, key: str, data: ManagedEntry) -> None:
@@ -391,7 +390,20 @@
return load_from_json(json_str=body)
-class FileTreeStore(BaseStore):
+async def iter_key_files(directory: AsyncPath) -> AsyncGenerator[AsyncPath]:
+ """Yield each key file (`{key}.json`) directly inside a collection
directory.
+
+ Collection metadata lives in a separate metadata directory as
`{collection}-info.json`,
+ never as `info.json` inside the collection directory itself, so a key
literally named
+ "info" is a normal key file here, not metadata.
+ """
+ async for item_path in directory.iterdir():
+ if not await item_path.is_file() or item_path.suffix != ".json":
+ continue
+ yield item_path
+
+
+class FileTreeStore(BaseCullStore):
"""A file-tree based store using directories for collections and files for
keys.
This store uses the native filesystem:
@@ -425,8 +437,12 @@
- No file locking: Concurrent writes to the same key from multiple
processes may
cause data loss (last write wins). Single-writer or external locking
is recommended
for multi-process scenarios.
- - No built-in cleanup of expired entries. Expired entries are only
filtered out when
- read via get() or similar methods.
+ - Expired entries are filtered out when read via get() or similar
methods, but the
+ underlying file is not removed until something reads the key or
`cull()` is called.
+ Call `cull()` periodically (e.g. from a scheduled task) to reclaim
disk space from
+ write-once, never-reread keys. `cull()` only looks one directory
level below the data
+ directory, so a collection name containing a path separator (only
possible with a
+ permissive sanitization strategy like PassthroughStrategy) won't be
swept.
- Performance may degrade with very large numbers of keys per
collection due to
filesystem directory entry limits.
"""
@@ -591,3 +607,55 @@
collection_info: DiskCollectionInfo =
self._collection_infos[collection]
return await collection_info.delete_entry(key=key)
+
+ @override
+ async def _cull(self) -> None:
+ """Delete expired entries from disk.
+
+ Walks the data directory itself rather than the collection index, so
it also reclaims
+ space for collections this store instance has never set up. Collection
names containing
+ path separators (only possible with a permissive sanitization strategy
like
+ PassthroughStrategy) nest their key files deeper than this walks --
not covered here.
+ """
+ async for collection_directory in self._get_data_directories():
+ try:
+ await
validate_path_within_directory(path=collection_directory,
root_directory=self._data_directory)
+ except PathSecurityError:
+ # A symlink escapes the store root; don't follow it.
+ continue
+
+ async for file_path in iter_key_files(collection_directory):
+ try:
+ await validate_path_within_directory(path=file_path,
root_directory=self._data_directory)
+ except PathSecurityError:
+ # Same, for a symlinked file inside an
otherwise-legitimate collection.
+ continue
+
+ try:
+ # Stat before reading so it reflects what we're about to
read, not whatever a
+ # concurrent put() has since replaced this path with.
+ stat_at_read = await file_path.stat()
+ data_dict: dict[str, Any] = await read_file(file=file_path)
+ entry: ManagedEntry =
self._serialization_adapter.load_dict(data=data_dict)
+ except FileNotFoundError:
+ continue
+ except Exception as e:
+ # A file that doesn't parse as a ManagedEntry shouldn't
stop the rest of the sweep.
+ logger.warning(
+ "Skipping unparseable file during cull",
+ extra={"file": str(file_path), "error": str(e)},
+ exc_info=True,
+ )
+ continue
+
+ if not entry.is_expired:
+ continue
+
+ with contextlib.suppress(FileNotFoundError):
+ stat_before_unlink = await file_path.stat()
+ # Skip the delete if the file was replaced since we read
it, so a concurrent
+ # put() landing here can't have its fresh value deleted.
Compare both identity
+ # markers: mtime resolution alone could theoretically
collide, and inode alone
+ # is 0 on some filesystems, so either one changing is
enough to detect a replace.
+ if (stat_before_unlink.st_ino,
stat_before_unlink.st_mtime_ns) == (stat_at_read.st_ino,
stat_at_read.st_mtime_ns):
+ await file_path.unlink()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/py_key_value_aio-0.4.5/src/key_value/aio/stores/redis/store.py
new/py_key_value_aio-0.4.6/src/key_value/aio/stores/redis/store.py
--- old/py_key_value_aio-0.4.5/src/key_value/aio/stores/redis/store.py
1970-01-01 01:00:00.000000000 +0100
+++ new/py_key_value_aio-0.4.6/src/key_value/aio/stores/redis/store.py
1970-01-01 01:00:00.000000000 +0100
@@ -212,6 +212,7 @@
host: str = "localhost",
port: int = 6379,
db: int = 0,
+ username: str | None = None,
password: str | None = None,
ssl: bool = False,
ssl_ca_certs: str | None = None,
@@ -232,6 +233,7 @@
host: str = "localhost",
port: int = 6379,
db: int = 0,
+ username: str | None = None,
password: str | None = None,
ssl: bool = False,
ssl_ca_certs: str | None = None,
@@ -252,6 +254,7 @@
host: Redis host. Defaults to localhost.
port: Redis port. Defaults to 6379.
db: Redis database number. Defaults to 0.
+ username: Redis username. Defaults to None (Redis's "default"
user).
password: Redis password. Defaults to None.
ssl: Enable SSL/TLS for the connection. Defaults to False. Not
needed
when using a ``rediss://`` URL (SSL is inferred from the
scheme).
@@ -287,6 +290,7 @@
host=host,
port=port,
db=db,
+ username=username,
password=password,
ssl_enabled=ssl,
ssl_ca_certs=ssl_ca_certs,