- cloud_analysis_versioning.py: compute_version_key with version>etag>fingerprint precedence (D-19); compute_fingerprint helper; content_hash is optional post- hydration only (D-20); raises ValueError only, no HTTPException - cloud_analysis_settings.py: get_or_create_user_analysis_settings (upsert on first access), update_user_analysis_settings with enum validation and tier-capped cache limit, build_default_settings for API responses; raises ValueError only - cloud_cache.py: Phase 14 durable cache functions added alongside existing Phase 12 in-memory TTL cache — create_cache_entry, list_cache_entries (owner-scoped), evict_lru_entries (LRU, skips pin_count>0 and active_job_count>0), increment/ decrement_quota_for_cache (atomic UPDATE pattern); re-exports compute_version_key from cloud_analysis_versioning so tests have one import site
133 lines
5.2 KiB
Python
133 lines
5.2 KiB
Python
"""
|
|
Cloud analysis version-key helper — Phase 14.
|
|
|
|
Provides compute_version_key: the single canonical helper for deriving the
|
|
idempotency key that determines whether a cloud item has changed since its
|
|
last analysis.
|
|
|
|
Version key precedence (PATTERNS.md, D-19):
|
|
1. provider `version` field (most reliable — survives filename/path changes)
|
|
2. provider `etag` (HTTP-standard opaque identifier from HEAD/metadata)
|
|
3. metadata fingerprint — deterministic hash of (provider_item_id, size,
|
|
modified_at, content_type) — used when provider supplies neither version
|
|
nor etag
|
|
4. content_hash — accepted as an ADDITIONAL post-hydration fact, never
|
|
required before bytes are downloaded (D-20)
|
|
|
|
Rules:
|
|
- Service raises ValueError only — never HTTPException (CLAUDE.md).
|
|
- Content hash is NEVER used as a fallback that would require byte download.
|
|
- The same inputs must always produce the same key (deterministic).
|
|
- Different inputs must reliably produce different keys.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from typing import Optional
|
|
|
|
|
|
# ── Public constant ───────────────────────────────────────────────────────────
|
|
|
|
#: Prefix tags that identify which precedence level produced the key.
|
|
#: These are load-bearing — callers can strip the prefix to get the raw value,
|
|
#: or compare keys with mismatched prefixes without hashing collisions.
|
|
_PREFIX_VERSION = "v:"
|
|
_PREFIX_ETAG = "e:"
|
|
_PREFIX_FINGERPRINT = "fp:"
|
|
_PREFIX_CONTENT_HASH = "ch:"
|
|
|
|
|
|
def compute_version_key(
|
|
*,
|
|
provider_item_id: str,
|
|
version: Optional[str],
|
|
etag: Optional[str],
|
|
size: Optional[int],
|
|
modified_at: Optional[str],
|
|
content_type: Optional[str],
|
|
content_hash: Optional[str] = None,
|
|
) -> str:
|
|
"""Return the canonical version key for a cloud item.
|
|
|
|
The key uniquely identifies the content state of a provider item.
|
|
Passing content_hash is optional and only enhances the fingerprint
|
|
after bytes have already been hydrated — it is never a precondition
|
|
for calling this function.
|
|
|
|
Args:
|
|
provider_item_id: Opaque provider-assigned item identifier.
|
|
version: Provider version string (e.g. Google Drive `version`).
|
|
Takes precedence over etag.
|
|
etag: Provider etag (e.g. OneDrive/WebDAV ETag header).
|
|
Used when version is None.
|
|
size: Provider-reported byte size. Used in the fingerprint
|
|
fallback.
|
|
modified_at: Provider-reported last-modified timestamp string.
|
|
Used in the fingerprint fallback.
|
|
content_type: MIME content type. Used in the fingerprint fallback.
|
|
content_hash: Optional SHA-256 or similar hash computed after bytes
|
|
are already hydrated. When provided alongside a
|
|
version or etag, it is appended to the key to allow
|
|
post-analysis re-keying without triggering a new
|
|
download. When provided without version or etag, it
|
|
is used as a supplementary fingerprint component.
|
|
|
|
Returns:
|
|
A non-empty string that uniquely represents the item's current content
|
|
state at the given precedence level.
|
|
|
|
Raises:
|
|
ValueError: provider_item_id is empty or None.
|
|
"""
|
|
if not provider_item_id:
|
|
raise ValueError("provider_item_id must not be empty")
|
|
|
|
if version:
|
|
base = f"{_PREFIX_VERSION}{version}"
|
|
elif etag:
|
|
base = f"{_PREFIX_ETAG}{etag}"
|
|
else:
|
|
base = _compute_fingerprint(provider_item_id, size, modified_at, content_type)
|
|
|
|
# Post-hydration content hash enhances the key without requiring a new download.
|
|
if content_hash:
|
|
base = f"{base}|{_PREFIX_CONTENT_HASH}{content_hash}"
|
|
|
|
return base
|
|
|
|
|
|
def compute_fingerprint(
|
|
provider_item_id: str,
|
|
size: Optional[int],
|
|
modified_at: Optional[str],
|
|
content_type: Optional[str],
|
|
) -> str:
|
|
"""Return the stable metadata fingerprint for an item lacking version/etag.
|
|
|
|
Public entry point for callers that need only the fingerprint portion
|
|
(e.g. when pre-computing the cloud_analysis_job_items.fingerprint field).
|
|
|
|
The fingerprint is a hex-encoded SHA-256 of the canonical field string.
|
|
"""
|
|
return _compute_fingerprint(provider_item_id, size, modified_at, content_type)
|
|
|
|
|
|
# ── Internal helpers ──────────────────────────────────────────────────────────
|
|
|
|
def _compute_fingerprint(
|
|
provider_item_id: str,
|
|
size: Optional[int],
|
|
modified_at: Optional[str],
|
|
content_type: Optional[str],
|
|
) -> str:
|
|
"""Return a prefixed SHA-256 fingerprint of item metadata."""
|
|
# Canonical form: fields joined with ':' using empty string for None
|
|
canonical = ":".join([
|
|
provider_item_id,
|
|
str(size) if size is not None else "",
|
|
modified_at or "",
|
|
content_type or "",
|
|
])
|
|
digest = hashlib.sha256(canonical.encode()).hexdigest()
|
|
return f"{_PREFIX_FINGERPRINT}{digest}"
|