ó
    ¨ô#jˆ  ã                  ó°   • % S r SSKJr  SSKrSSKJr  SSKJrJrJ	r	  SSK
Jr  \" S5      rSrS	\S
'   \\4     SS jjr\\4     SS jjr\4SS jjrg)u%  TTL-aware S3 ingestion cache (QUE-223).

Registers a persistent-cache ``cloudpathlib.S3Client`` as the process-global
default for ``s3://`` URIs, so any ``CloudPath("s3://...")`` /
``AnyPath("s3://...")`` materializes through a project-local cache dir instead
of re-downloading every run. Source-change correctness is cloudpathlib's
built-in ETag check; disk hygiene is a wall-clock TTL enforced by a single
startup sweep.

No subclass, no per-access hook â€” one module, two functions, two constants:

- ``evict_stale_s3_cache()`` â€” startup sweep. Walks the cache dir and unlinks
  any file whose access time is older than the TTL. This is the *only* TTL
  reclamation path: cloudpathlib's ETag check handles source correctness; the
  sweep handles "downloaded once, never touched again" files that would
  otherwise live in the cache forever.
- ``init_s3_cache()`` â€” package-import entrypoint. Runs the sweep, then
  constructs and registers the default S3 client. Called once from
  ``quber/__init__.py``.

TTL is keyed off ``st_atime``, so re-processing a cached document resets its
window: actively used files stay warm, abandoned ones age out after the TTL.
On ``noatime``-mounted volumes atime is frozen and TTL degrades to "time since
download" â€” accepted rather than adding mount-detection complexity.
é    )ÚannotationsN)ÚPath)ÚAnyPathÚS3ClientÚS3Path)Úloggerz	.cache/s3i€— ÚfloatÚTTL_SECONDSc                ó.  • U R                  5       (       d  g[        R                  " 5       U-
  nSnU R                  S5       HO  nUR                  5       (       d  M  UR	                  5       R
                  U:  d  M:  UR                  5         US-  nMQ     U$ )aD  Delete cached files whose atime is older than ``ttl_seconds``.

Walks ``cache_dir`` once with ``rglob('*')`` and unlinks regular files past
the TTL. Returns the count evicted. Idempotent and safe to call when the
cache directory does not exist yet (returns 0). Linear in the cached-file
count; sub-second even at thousands.
r   Ú*é   )ÚexistsÚtimeÚrglobÚis_fileÚstatÚst_atimeÚunlink)Ú	cache_dirÚttl_secondsÚcutoffÚevictedÚps        Ú)/home/mande/repo/quber/src/quber/cache.pyÚevict_stale_s3_cacher   '   su   € ð ×Ñ×ÑØÜ�YŠY‹[˜;Ñ&€FØ€GØ�_‰_˜SÖ!ˆØ�9‰9�;‹;˜1Ÿ6™6›8×,Ñ,¨vÕ5Ø�H‰HŒJØ�q‰LŠGñ "ð €Nó    c                ó†   • [        X5      nU(       a  [        R                  " SX 5        [        U SS9R	                  5         g)uŠ  Prune the stale cache, then register the default S3 client.

Called once at package import (see ``quber/__init__.py``). Two ordered
steps:

1. ``evict_stale_s3_cache()`` â€” bound disk usage for keys not re-accessed
   between runs.
2. Construct a persistent-cache ``S3Client`` rooted at ``cache_dir`` and
   publish it via ``set_as_default_client()`` so plain
   ``CloudPath("s3://...")`` / ``AnyPath("s3://...")`` picks it up.

Constructing the client requires neither AWS credentials nor network access
(boto3 resolves both lazily on first GET), so this is safe to run at import
on any host, including CPU-only / credential-less dev environments.
z,Evicted {} stale file(s) from S3 cache at {}Ú
persistent)Úlocal_cache_dirÚfile_cache_modeN)r   r   Údebugr   Úset_as_default_client)r   r   r   s      r   Úinit_s3_cacher#   =   s9   € ô& # 9Ó:€GÞÜ�ŠÐCÀWÔXÜØ!Ø$ñ÷ ÑÕr   c                ó  • [        U 5      n[        U[        5      (       a_  XR                  -  UR                  -  nUR                  5       (       a  [        R                  " SX#5        O[        R                  " SX#5        [        U5      $ )ai  Coerce a user-supplied path string to a local filesystem Path.

Local inputs pass straight through. For ``s3://`` inputs, log an INFO line
when the document is already present in the local cache (cache hit), then
materialize it (cloudpathlib downloads on miss) and return the local Path.

The cache-hit check mirrors cloudpathlib's on-disk layout
(``<cache_dir>/<bucket>/<key>``) using only public ``S3Path`` attributes.
Note: cloudpathlib still revalidates the S3 ETag on access, so a logged hit
means "present locally"; if the source object changed upstream, cloudpathlib
transparently re-downloads the fresh copy.
zS3 cache hit: {} served from {}z#S3 cache miss: downloading {} to {})
r   Ú
isinstancer   ÚbucketÚkeyr   r   Úinfor!   r   )Úrawr   ÚdocÚcacheds       r   Úresolve_documentr,   Y   sb   € ô �#‹,€CÜ�#”v×ÑØŸZ™ZÑ'¨#¯'©'Ñ1ˆØ�=‰=�?‰?Ü�KŠKÐ9¸3ÕGä�LŠLÐ>ÀÔLÜ�‹9Ðr   )r   r   r   r	   ÚreturnÚint)r   r   r   r	   r-   ÚNone)r)   Ústrr   r   r-   r   )Ú__doc__Ú
__future__r   r   Úpathlibr   Úcloudpathlibr   r   r   Úlogurur   Ú	CACHE_DIRr
   Ú__annotations__r   r#   r,   © r   r   Ú<module>r9      sŠ   ðòõ4 #ã Ý ç 2Ñ 2Ý á�Ó€	Ø€ˆUÓ ð  Ø$ðØðàðð 	õð.  Ø$ðØðàðð 
õð8 2;÷ r   