Build and Push Docker Container / build-and-push (push) Successful in 2m25s
- 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+.
52 lines
1.7 KiB
Python
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))
|