Files
simple-nanoshare/my_modules/filecache/metadata.py
T
daniel156161 73fdeacbfe
Build and Push Docker Container / build-and-push (push) Successful in 2m25s
feat!: adopt Python 3.14 and 3.15 language features
- Require Python 3.15 for the server and Python 3.14 for the CLI.
- Match the server requirement to quart_common's Python 3.15 features.
- Replace future annotations with native deferred evaluation.
- Defer CLI command dependencies and QR imports on Python 3.15.
- Enable argparse suggestions for mistyped CLI commands.
- Remove the obsolete tomllib import fallback.
- Update my_helpers and quart_common submodule references.
- Lock test dependencies and exclude them from Docker installations.
- Add regression coverage for lazy imports and deferred annotations.
- Document runtime requirements and standalone CLI validation.
- Validate 276 tests on Python 3.15 and 18 CLI tests on Python 3.14.

BREAKING CHANGE: The server now requires Python 3.15+ and the standalone CLI requires Python 3.14+.
2026-10-26 14:47:29 +01:00

52 lines
1.7 KiB
Python

from collections.abc import Awaitable, Callable
from .validators import remaining_seconds
class FileMetadataCache:
"""Cache-aside layer for convex.get_file, shared across workers via AsyncCache.
Only complete, non-expired records are cached. Misses and expired markers stay
uncached so a newly uploaded or just-expired file is never masked.
"""
def __init__(self, cache, ttl: int = 60, prefix: str = 'nanoshare:filemeta:'):
self._cache = cache
self._ttl = max(1, int(ttl))
self._prefix = prefix
self.hits = 0
self.misses = 0
def _key(self, file_id: str) -> str:
return f'{self._prefix}{file_id}'
def _effective_ttl(self, file_data: dict) -> int:
"""Never hold a record past the moment it expires."""
remaining = remaining_seconds(file_data.get('expires_at'))
if remaining is None:
return self._ttl
return min(self._ttl, remaining)
def _is_cacheable(self, file_data) -> bool:
if not isinstance(file_data, dict) or not file_data:
return False
if file_data.get('expired'):
return False
return bool(file_data.get('db_image_url'))
async def get(self, file_id: str, loader: Callable[[], Awaitable[dict | None]]) -> dict | None:
cached = await self._cache.get(self._key(file_id))
if isinstance(cached, dict) and cached:
self.hits += 1
return cached
self.misses += 1
file_data = await loader()
if self._is_cacheable(file_data):
ttl = self._effective_ttl(file_data)
if ttl > 0:
await self._cache.set(self._key(file_id), file_data, ttl=ttl)
return file_data
async def invalidate(self, file_id: str) -> None:
await self._cache.delete(self._key(file_id))