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,

Reply via email to